spg_sql/parser.rs
1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21 AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22 CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23 CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24 ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25 FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26 FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27 GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28 NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29 PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30 Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31 WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38 Vec<Expr>,
39 Vec<(Expr, bool, Option<bool>)>,
40 Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50 if grant {
51 Statement::Grant(g)
52 } else {
53 Statement::Revoke(g)
54 }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58 matches!(
59 lc,
60 // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61 // real statement with a real store. v7.39 (read01 round 57): "grant" /
62 // "revoke" moved OUT — table privileges are now REAL (stored in
63 // `relacl`, enforced against the session role); a grant on any other
64 // object class still parses and no-ops so dumps restore.
65 // MySQL bulk-load brackets.
66 "unlock"
67 // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68 // diagnostics that pg_dump-style tools also emit
69 // post-restore.
70 | "optimize"
71 | "check"
72 | "use"
73 // PG psql backslash meta-commands that newer
74 // pg_dump versions emit unescaped (\restrict /
75 // \unrestrict). Real psql intercepts these; SPG's
76 // PG-wire sees them as raw text.
77 | "\\restrict"
78 | "\\unrestrict"
79 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80 // `DELIMITER ;` directives. Technically client-side
81 // (the `mysql` CLI uses them to set the statement
82 // terminator), not SQL — but mysqldump and stored-
83 // procedure scripts emit them inline. SPG's parser
84 // sees one statement at a time and doesn't care
85 // about the terminator, so consume DELIMITER lines
86 // as Empty.
87 | "delimiter"
88 // v7.37.17 (17.6 siblings) — additional PG maintenance /
89 // session-state statements pg_dump + application startup
90 // scripts emit. SPG has no matching session-state to
91 // discard (no prepared-plan cache surface, no temp
92 // sequences), no matching security-label / storage-
93 // option to apply, no separate CREATE/DROP CAST that
94 // affects execution.
95 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96 // pg_dump / pg_dumpall emit around DROP ROLE:
97 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
98 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99 // Both operate on the role's owned objects; SPG has no
100 // role-owner model, so accept-and-no-op.
101 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102 // + extension scripts use LOAD to preload shared
103 // libraries. SPG doesn't have a shared-library extension
104 // point today (extensions ship as first-class crates
105 // linked at build time); accept as a no-op.
106 | "load"
107 )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133 let s = match tok {
134 // PG keyword class: unreserved or col_name.
135 //
136 Token::Release => "release",
137 Token::Savepoint => "savepoint",
138 Token::Show => "show",
139 Token::Index => "index",
140 Token::Begin => "begin",
141 Token::Commit => "commit",
142 Token::Rollback => "rollback",
143 Token::Drop => "drop",
144 Token::Insert => "insert",
145 Token::Values => "values",
146 Token::Limit => "limit",
147 Token::Partition => "partition",
148 Token::Tables => "tables",
149 Token::Connection => "connection",
150 Token::Publication => "publication",
151 Token::Subscription => "subscription",
152 Token::Interval => "interval",
153 // `extract` is non-reserved in PG too (it's a function the
154 // parser dispatches via context — outside that context it's
155 // a plain identifier).
156 Token::Extract => "extract",
157 Token::Offset => "offset",
158 // `to` is reserved in PG (used in many "AS … TO …" forms), so
159 // it is NOT relaxed here. Same for `from`, `where`, `as`,
160 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163 // `left`, `cross`, `outer`, `default`, `is`, `between`,
164 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165 // (partial — keep partition as unreserved per modern PG).
166 _ => return None,
167 };
168 Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183 s.eq_ignore_ascii_case("jsonb_each_text")
184 || s.eq_ignore_ascii_case("jsonb_each")
185 || s.eq_ignore_ascii_case("json_each_text")
186 || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195 fname: &str,
196 args: Vec<Expr>,
197 names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200 "make_date" => &["year", "month", "day"],
201 "make_time" => &["hour", "min", "sec"],
202 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204 other => {
205 return Err(alloc::format!(
206 "function {other}(...) does not support named arguments"
207 ));
208 }
209 };
210 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211 let mut next_positional = 0usize;
212 for (arg, name) in args.into_iter().zip(names.iter()) {
213 let idx = match name {
214 Some(n) => params
215 .iter()
216 .position(|p| p.eq_ignore_ascii_case(n))
217 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218 None => {
219 let i = next_positional;
220 next_positional += 1;
221 i
222 }
223 };
224 if idx >= slots.len() {
225 return Err(alloc::format!("too many arguments for {fname}(...)"));
226 }
227 if slots[idx].is_some() {
228 return Err(alloc::format!(
229 "argument \"{}\" specified more than once",
230 params[idx]
231 ));
232 }
233 slots[idx] = Some(arg);
234 }
235 Ok(slots
236 .into_iter()
237 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238 .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249 Expanded(String),
250 /// Exponent pushes the value outside PG's numeric format
251 /// (more than 131072 integer digits or 16383 fractional digits).
252 Overflow,
253 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254 NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262 let s = s.trim();
263 let Some(epos) = s.find(['e', 'E']) else {
264 return SciExpanded::NotScientific;
265 };
266 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267 let Ok(exp) = exp_str.parse::<i64>() else {
268 return SciExpanded::NotScientific;
269 };
270 let (neg, mant) = match mant.strip_prefix('-') {
271 Some(r) => (true, r),
272 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273 };
274 let (int_part, frac_part) = match mant.split_once('.') {
275 Some((i, f)) => (i, f),
276 None => (mant, ""),
277 };
278 if (int_part.is_empty() && frac_part.is_empty())
279 || !int_part.bytes().all(|b| b.is_ascii_digit())
280 || !frac_part.bytes().all(|b| b.is_ascii_digit())
281 {
282 return SciExpanded::NotScientific;
283 }
284 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285 digits.push_str(int_part);
286 digits.push_str(frac_part);
287 // Decimal point position within `digits` after applying the exponent.
288 let new_point = int_part.len() as i64 + exp;
289 // PG's numeric format: up to 131072 digits before the point, 16383 after.
290 if new_point > 131_072 {
291 return SciExpanded::Overflow;
292 }
293 if (digits.len() as i64 - new_point) > 16_383 {
294 return SciExpanded::Overflow;
295 }
296 let sign = if neg { "-" } else { "" };
297 let plain = if new_point <= 0 {
298 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299 out.push_str("0.");
300 for _ in 0..(-new_point) {
301 out.push('0');
302 }
303 out.push_str(&digits);
304 out
305 } else if (new_point as usize) >= digits.len() {
306 let mut out = digits;
307 for _ in 0..(new_point as usize - out.len()) {
308 out.push('0');
309 }
310 out
311 } else {
312 let mut out = String::with_capacity(digits.len() + 1);
313 out.push_str(&digits[..new_point as usize]);
314 out.push('.');
315 out.push_str(&digits[new_point as usize..]);
316 out
317 };
318 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327 let padded = if hex.len() % 2 == 1 {
328 alloc::format!("0{hex}")
329 } else {
330 hex.to_string()
331 };
332 Expr::Cast {
333 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334 "\\x{padded}"
335 )))),
336 target: CastTarget::Named("bytea".to_string()),
337 }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345 let pad = (8 - bits.len() % 8) % 8;
346 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348 for nibble in padded.as_bytes().chunks(4) {
349 let mut v = 0u8;
350 for &b in nibble {
351 v = (v << 1) | (b - b'0');
352 }
353 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354 }
355 hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365 match parse_decimal_literal(&s) {
366 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368 // its exact value as a NumericBig.
369 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370 // v7.39 (read01 numeric.c) — expand the exponent form.
371 None => match expand_scientific_literal(&s) {
372 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374 None if plain
375 .split_once('.')
376 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377 {
378 Ok(Literal::NumericBig(plain))
379 }
380 None => s
381 .parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| format!("invalid numeric literal {s:?}")),
384 },
385 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386 SciExpanded::NotScientific => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395 let (int_part, frac_part) = match s.split_once('.') {
396 Some((i, f)) => (i, f),
397 None => (s, ""),
398 };
399 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400 // places fell out of the numeric path here, which is why
401 // `pg_typeof(1e-256)` answered double precision and a plain
402 // 256-place decimal aborted the query in the big-decimal converter.
403 if frac_part.len() > u16::MAX as usize {
404 return None;
405 }
406 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407 digits.push_str(int_part);
408 digits.push_str(frac_part);
409 let mantissa: i128 = digits.parse().ok()?;
410 #[allow(clippy::cast_possible_truncation)]
411 Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418 s.eq_ignore_ascii_case("jsonb_to_recordset")
419 || s.eq_ignore_ascii_case("jsonb_to_record")
420 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421 // column-definition list desugars identically (the record base
422 // argument only carries the type; a non-NULL base's field
423 // defaults are a recorded delta).
424 || s.eq_ignore_ascii_case("json_populate_record")
425 || s.eq_ignore_ascii_case("jsonb_populate_record")
426 || s.eq_ignore_ascii_case("json_populate_recordset")
427 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428 || s.eq_ignore_ascii_case("json_to_recordset")
429 || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433 /// Whether what follows an identifier ends an index key, which is how
434 /// an operator class is told from anything else in that position.
435 fn opclass_position_follows(next: Option<&Token>) -> bool {
436 match next {
437 // `ASC` / `DESC` have their own tokens; matching them as
438 // identifiers named "asc" / "desc" — which the first version of
439 // this did — never fires, and `(c text_pattern_ops DESC)` (which
440 // PG18.4 accepts, verified) went on failing to parse.
441 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442 Some(Token::Ident(w)) => {
443 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444 }
445 _ => false,
446 }
447 }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451 let lc = name.to_ascii_lowercase();
452 matches!(
453 lc.as_str(),
454 "vector_cosine_ops"
455 | "vector_l2_ops"
456 | "vector_ip_ops"
457 | "halfvec_cosine_ops"
458 | "halfvec_l2_ops"
459 | "halfvec_ip_ops"
460 | "sq8_cosine_ops"
461 | "sq8_l2_ops"
462 | "sq8_ip_ops"
463 // pg_trgm — trigram operator class. SPG's GIN index
464 // already uses tsvector tokens; trigram-style LIKE
465 // pattern matching still routes through a sequential
466 // scan, but the opclass name is accepted so PG schemas
467 // load.
468 | "gin_trgm_ops"
469 | "gist_trgm_ops"
470 // PG built-in btree opclasses occasionally appear in
471 // pg_dump output for column types with multiple
472 // sort orders (text_pattern_ops, varchar_pattern_ops,
473 // bpchar_pattern_ops).
474 | "text_pattern_ops"
475 | "varchar_pattern_ops"
476 | "bpchar_pattern_ops"
477 | "int4_ops"
478 | "int8_ops"
479 | "text_ops"
480 )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485 pub message: String,
486 /// Index into the token stream where parsing tripped. Not a byte offset.
487 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488 /// field would grow every `Result<_, ParseError>` slot on the deeply
489 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490 /// 1-based char position is recovered on the cold error path by
491 /// [`syntax_error_position`], which re-tokenizes to map this token index.
492 pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497 /// with `parse error at token #N: `, which PG has no equivalent of:
498 /// the message bodies are already PG's verbatim (`LIMIT must not be
499 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500 /// prefix was SPG's internal token index leaking into every one of
501 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502 /// character position from it for the ErrorResponse `P`.
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str(&self.message)
505 }
506}
507
508impl From<LexError> for ParseError {
509 fn from(e: LexError) -> Self {
510 Self {
511 message: format!("lex: {e}"),
512 token_pos: 0,
513 }
514 }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523 let (tokens, offsets) =
524 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525 let mut p = Parser::new(tokens);
526 let expr = p
527 .parse_expr(0)
528 .and_then(|e| p.expect_eof().map(|()| e))
529 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530 Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536 parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544 .map_err(|e| shape_lex_error(&e, input))?;
545 // The same session flag names the dialect for both the lexer and
546 // the type mapping.
547 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548 let stmt = (|| {
549 let stmt = p.parse_one_statement()?;
550 if matches!(p.peek(), Token::Semicolon) {
551 p.advance();
552 }
553 p.expect_eof()?;
554 Ok(stmt)
555 })()
556 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557 Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575 return e;
576 }
577 let message = match offending_lexeme(input, offsets, e.token_pos) {
578 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579 None => "syntax error at end of input".into(),
580 };
581 ParseError {
582 message,
583 token_pos: e.token_pos,
584 }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595 use lexer::LexErrorKind as K;
596 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597 let message = match &e.kind {
598 K::UnterminatedString => {
599 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600 }
601 K::UnterminatedQuotedIdent => {
602 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603 }
604 K::UnterminatedBlockComment => {
605 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606 }
607 // PG has no "unknown character" error of its own — the character
608 // is skipped and the parser reports the next token. SPG stops at
609 // the character itself and names it, which is the same shape.
610 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611 // The number-literal kinds already carry PG's `at or near` form.
612 other => alloc::format!(
613 "{}",
614 lexer::LexError {
615 kind: other.clone(),
616 pos: e.pos,
617 }
618 ),
619 };
620 ParseError {
621 message,
622 token_pos: 0,
623 }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630 let start = *offsets.get(token_pos)?;
631 if start >= input.len() {
632 return None;
633 }
634 let end = offsets
635 .get(token_pos + 1)
636 .copied()
637 .unwrap_or(input.len())
638 .min(input.len());
639 let seg = input.get(start..end)?.trim();
640 if seg.is_empty() {
641 return None;
642 }
643 // A quoted literal / identifier keeps its inner spaces; anything else
644 // ends at the first whitespace (the segment runs to the NEXT token's
645 // start, which may swallow a comment).
646 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647 Some(seg)
648 } else {
649 seg.split_whitespace().next()
650 }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663 input: &str,
664 backslash_escapes: bool,
665 token_pos: usize,
666) -> Option<usize> {
667 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668 let byte_off = *offsets.get(token_pos)?;
669 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670 return None;
671 }
672 Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676 tokens: Vec<Token>,
677 pos: usize,
678 /// v7.39 (round 274) — the session's dialect, carried by the same
679 /// signal that drives string-literal escaping: `SET sql_mode` (only
680 /// MySQL clients and mysqldump preambles emit it) turns it on,
681 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682 /// it off. Needed here because the two dialects disagree about what
683 /// `REAL` means — see the type mapping below.
684 mysql_dialect: bool,
685 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686 /// mutually recursive expr/select parsers. Bounded so a deeply
687 /// nested input returns a parse error instead of overflowing
688 /// the stack (embed hosts die on overflow — it is an abort,
689 /// not a catchable error).
690 nest_depth: usize,
691 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692 /// `random() < p/100` predicate here; the enclosing SELECT
693 /// drains the list after its WHERE parses and ANDs the
694 /// predicates in. parse_bare_select save/restores around its
695 /// FROM+WHERE so nested selects only drain their own.
696 pending_sample_preds: Vec<Expr>,
697 /// v7.39 (round 691) — collation lowering channel, the same shape as
698 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699 /// information, and `ast::OrderBy` is where this parser keeps ordering
700 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701 /// variant — puts a new arm on `eval_expr`, which this repo has
702 /// measured to overflow the debug stack. So while an ORDER BY KEY is
703 /// being parsed the postfix loop drops the name here instead of
704 /// refusing it, and the key's parser takes it.
705 ///
706 /// Only inside an ORDER BY key: everywhere else an unperformable
707 /// collation still errors, because accepting one at a COMPARISON and
708 /// ignoring it is the defect F36 exists to close.
709 in_order_by_key: bool,
710 order_key_collation: Option<String>,
711 /// POSITION(sub IN str) — while parsing the needle, the IN
712 /// keyword is the argument separator, not a membership test.
713 /// The postfix loop leaves IN unconsumed when this is set.
714 suppress_in_tail: bool,
715 /// Index of the token the last `advance()` returned — see
716 /// [`Parser::consumed_pos`].
717 last_consumed: usize,
718 /// v7.39 (round 506) — the statement's own text and the byte each token
719 /// starts at, so a MySQL projection item can report the SOURCE TEXT
720 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
721 /// spacing and all. Only filled for a MySQL session — a PG one names
722 /// columns from the parsed shape and pays nothing for this.
723 src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739/// this budget and errors. Verified against a live server for nested
740/// derived tables, parens, calls, CASE, IN-subqueries, scalar
741/// subqueries, NOT and unary minus — the server stayed up through all
742/// of them. This is the contract that matters, and it holds.
743/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745/// and executing aborts around 8 inside a test thread. The budget is
746/// simply unreachable there, which is why a deep-nesting test has to
747/// ask for a large stack of its own — see `nesting_budget_errors_at`
748/// in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767 "pg_am",
768 "pg_attrdef",
769 "pg_attribute",
770 "pg_cast",
771 "pg_db_role_setting",
772 "pg_conversion",
773 "pg_default_acl",
774 "pg_shadow",
775 "pg_sequences",
776 "pg_range",
777 "pg_partitioned_table",
778 "pg_language",
779 "pg_group",
780 "pg_authid",
781 "pg_class",
782 "pg_collation",
783 "pg_constraint",
784 "pg_database",
785 "pg_depend",
786 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
787 "pg_description",
788 "pg_enum",
789 "pg_extension",
790 // v7.39 (round 541) — pg_dump reads it for every relation of kind
791 // 'f'. SPG has no foreign tables, so it is empty, which is also
792 // what PG reports on a database that has none.
793 "pg_foreign_table",
794 // v7.39 (round 541) — the empty-by-truth family; see
795 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
796 "pg_event_trigger",
797 "pg_file_settings",
798 "pg_foreign_data_wrapper",
799 "pg_foreign_server",
800 "pg_hba_file_rules",
801 "pg_ident_file_mappings",
802 "pg_init_privs",
803 "pg_parameter_acl",
804 "pg_prepared_xacts",
805 "pg_publication_namespace",
806 "pg_publication_rel",
807 "pg_publication_tables",
808 "pg_replication_origin",
809 "pg_replication_origin_status",
810 "pg_seclabel",
811 "pg_seclabels",
812 "pg_shdepend",
813 "pg_shdescription",
814 "pg_shmem_allocations",
815 "pg_shmem_allocations_numa",
816 "pg_shseclabel",
817 "pg_statistic_ext_data",
818 "pg_stats_ext",
819 "pg_stats_ext_exprs",
820 "pg_subscription_rel",
821 "pg_transform",
822 "pg_user_mapping",
823 "pg_user_mappings",
824 "pg_index",
825 "pg_indexes",
826 "pg_inherits",
827 // v7.39 (round 650) — the text-search catalogs SPG can fill
828 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
829 // token types to dictionaries and SPG has no token-type model,
830 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
831 "pg_ts_config",
832 "pg_ts_config_map",
833 "pg_ts_dict",
834 "pg_ts_parser",
835 "pg_ts_template",
836 "pg_matviews",
837 "pg_namespace",
838 // v7.39 (round 621)
839 "pg_operator",
840 "pg_policies",
841 "pg_policy",
842 "pg_proc",
843 "pg_publication",
844 "pg_replication_slots",
845 "pg_roles",
846 // v7.39 (round 143) — the rewrite-rule listing view.
847 // v7.39 (round 312) — and the rule catalogue itself, which
848 // `pg_get_ruledef(oid)` resolves against.
849 "pg_rewrite",
850 "pg_rules",
851 "pg_sequence",
852 "pg_settings",
853 "pg_stat_archiver",
854 "pg_stat_bgwriter",
855 "pg_stat_checkpointer",
856 "pg_stat_database",
857 "pg_stat_io",
858 "pg_stat_progress_analyze",
859 "pg_auth_members",
860 "pg_stat_progress_create_index",
861 "pg_stat_progress_vacuum",
862 "pg_stat_replication",
863 "pg_stat_slru",
864 "pg_stat_subscription_stats",
865 "pg_stat_user_functions",
866 "pg_stat_user_indexes",
867 "pg_stat_user_tables",
868 "pg_stat_wal",
869 "pg_prepared_statements",
870 "pg_largeobject",
871 "pg_largeobject_metadata",
872 "pg_statistic",
873 "pg_statistic_ext",
874 "pg_subscription",
875 "pg_tables",
876 "pg_tablespace",
877 // v7.39 (round 502) — the timezone catalogues. SPG resolved
878 // named zones correctly but could not list them, so a client
879 // populating a timezone picker got "relation does not exist".
880 "pg_timezone_abbrevs",
881 "pg_timezone_names",
882 "pg_trigger",
883 "pg_type",
884 "pg_user",
885 "pg_views",
886];
887
888const MAX_NEST_DEPTH: usize = 64;
889
890/// Stack accounting for the nesting budget, test-only.
891///
892/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
893/// that MOVES: a compiler upgrade grew the parser's debug frames and
894/// silently ate the margin until `nesting_budget_errors_cleanly` went
895/// from erroring cleanly to aborting on a stack overflow. A count
896/// cannot notice that on its own, so the budget is measured here and
897/// held to a ceiling.
898///
899/// The reading has to come from a helper whose OWN frame is the same at
900/// every call: debug slot placement does not follow source order, so a
901/// local's address inside the function under test is not that
902/// function's frame boundary. Two earlier probes were wrong that way —
903/// one read `&self.nest_depth`, which is the `Parser`'s address and
904/// never moves at all.
905#[cfg(test)]
906mod frame_meter {
907 extern crate std;
908 use std::cell::Cell;
909
910 // Per-THREAD, not global. `cargo test` runs tests in parallel and
911 // plenty of them parse nested expressions, so shared statics get
912 // stack addresses from several threads at once and the subtraction
913 // below turns into noise — it read 229,772 bytes per level that way,
914 // while passing when the test was run on its own.
915 std::thread_local! {
916 static AT_LO: Cell<usize> = const { Cell::new(0) };
917 static AT_HI: Cell<usize> = const { Cell::new(0) };
918 }
919
920 pub(super) const SAMPLE_LO: usize = 4;
921 pub(super) const SAMPLE_HI: usize = 24;
922
923 #[inline(never)]
924 pub(super) fn record(depth: usize) {
925 let anchor = 0u8;
926 let at = core::ptr::from_ref(&anchor) as usize;
927 if depth == SAMPLE_LO {
928 AT_LO.with(|c| c.set(at));
929 } else if depth == SAMPLE_HI {
930 AT_HI.with(|c| c.set(at));
931 }
932 }
933
934 /// Bytes of stack one nesting level costs, averaged over the span.
935 pub(super) fn bytes_per_level() -> usize {
936 let lo = AT_LO.with(Cell::get);
937 let hi = AT_HI.with(Cell::get);
938 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
939 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
940 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
941 }
942
943 pub(super) fn reset() {
944 AT_LO.with(|c| c.set(0));
945 AT_HI.with(|c| c.set(0));
946 }
947}
948
949/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
950/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
951#[inline(never)]
952fn build_center_call(e: Expr) -> Expr {
953 Expr::FunctionCall {
954 name: alloc::string::String::from("center"),
955 args: alloc::vec![e],
956 }
957}
958
959/// Max consecutive binary operators at ONE precedence level
960/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
961/// parse time but evaluates and drops recursively — depth beyond
962/// this overflows 2 MiB worker stacks (debug eval frames run
963/// multiple KiB). `IN (…)` lists are flat and unaffected.
964const MAX_BINARY_CHAIN: usize = 256;
965
966/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
967/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
968/// it keeps its dedicated path (`parse_table_level_fk`).
969enum NamedTableConstraintKind {
970 Check,
971 Unique,
972 PrimaryKey,
973 Exclude,
974}
975
976impl Parser {
977 fn new(tokens: Vec<Token>) -> Self {
978 Self::new_with_dialect(tokens, false)
979 }
980
981 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
982 Self {
983 tokens,
984 mysql_dialect,
985 in_order_by_key: false,
986 order_key_collation: None,
987 pos: 0,
988 nest_depth: 0,
989 pending_sample_preds: Vec::new(),
990 suppress_in_tail: false,
991 last_consumed: 0,
992 src: None,
993 }
994 }
995
996 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
997 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
998 if self.mysql_dialect {
999 self.src = Some((input.to_string(), offsets.to_vec()));
1000 }
1001 self
1002 }
1003
1004 /// The source text spanning tokens `start ..= end`, trimmed.
1005 ///
1006 /// The offsets are token STARTS, so the span runs to the start of the
1007 /// token after `end` and gives back the whitespace between them —
1008 /// trimming is what makes `a + b FROM t` end at `b`.
1009 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1010 let (text, offsets) = self.src.as_ref()?;
1011 let from = *offsets.get(start)?;
1012 let to = *offsets.get(end + 1)?;
1013 text.get(from..to).map(str::trim_end)
1014 }
1015
1016 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1017 /// nesting depth, erroring out cleanly past the budget.
1018 fn enter_nested(&mut self) -> Result<(), ParseError> {
1019 self.nest_depth += 1;
1020 #[cfg(test)]
1021 frame_meter::record(self.nest_depth);
1022 if self.nest_depth > MAX_NEST_DEPTH {
1023 self.nest_depth -= 1;
1024 return Err(self.err(alloc::format!(
1025 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1026 )));
1027 }
1028 Ok(())
1029 }
1030
1031 fn peek(&self) -> &Token {
1032 // tokens always ends with Eof; pos is clamped in advance().
1033 &self.tokens[self.pos]
1034 }
1035
1036 fn advance(&mut self) -> Token {
1037 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1038 self.last_consumed = self.pos;
1039 if self.pos + 1 < self.tokens.len() {
1040 self.pos += 1;
1041 }
1042 t
1043 }
1044
1045 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1046 /// returned. It was computed as `pos - 1`, which is wrong at both
1047 /// ends: `advance()` parks on the final Eof rather than running off
1048 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1049 /// input`), and after backtracking `pos` is no longer one past the
1050 /// token that failed. Recorded by `advance()` itself instead.
1051 const fn consumed_pos(&self) -> usize {
1052 self.last_consumed
1053 }
1054
1055 fn err(&self, message: String) -> ParseError {
1056 ParseError {
1057 message,
1058 token_pos: self.pos,
1059 }
1060 }
1061
1062 fn expect_eof(&self) -> Result<(), ParseError> {
1063 if matches!(self.peek(), Token::Eof) {
1064 Ok(())
1065 } else {
1066 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1067 }
1068 }
1069
1070 /// v7.14.0 — swallow every token up to (but not including) the
1071 /// next semicolon / EOF. Used by the dump-noise dispatcher
1072 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1073 /// etc. without modeling each grammar.
1074 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1075 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1076 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1077 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1078 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1079 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1080 let start = self.pos;
1081 self.advance(); // COMMENT
1082 if !matches!(self.peek(), Token::On) {
1083 self.pos = start;
1084 self.consume_until_statement_boundary();
1085 return Ok(Statement::Empty);
1086 }
1087 self.advance(); // ON
1088 let kind = match self.peek() {
1089 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1090 Token::Table => "table".into(),
1091 _ => {
1092 self.consume_until_statement_boundary();
1093 return Ok(Statement::Empty);
1094 }
1095 };
1096 if !matches!(
1097 kind.as_str(),
1098 "table"
1099 | "column"
1100 | "index"
1101 | "view"
1102 | "sequence"
1103 | "schema"
1104 | "type"
1105 | "database"
1106 | "function"
1107 ) {
1108 self.consume_until_statement_boundary();
1109 return Ok(Statement::Empty);
1110 }
1111 self.advance(); // the kind keyword
1112 // The object name. ⚠️ `expect_ident_like` strips a leading
1113 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1114 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1115 // `c`. Read the dotted parts from raw tokens instead, then let a
1116 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1117 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1118 loop {
1119 match self.advance() {
1120 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1121 other if unreserved_keyword_text(&other).is_some() => {
1122 parts.push(unreserved_keyword_text(&other).unwrap());
1123 }
1124 other => {
1125 return Err(ParseError {
1126 message: alloc::format!("expected identifier, got {other:?}"),
1127 token_pos: self.consumed_pos(),
1128 });
1129 }
1130 }
1131 if matches!(self.peek(), Token::Dot) {
1132 self.advance();
1133 } else {
1134 break;
1135 }
1136 }
1137 // COLUMN wants `table.column`; every other kind wants a bare name.
1138 let want = if kind == "column" { 2 } else { 1 };
1139 while parts.len() > want {
1140 parts.remove(0);
1141 }
1142 let name = parts.join(".");
1143 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1144 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1145 // error here — a dump carrying one function comment failed to
1146 // restore. The list is consumed (the comment store keys by name;
1147 // overload-precise comments are the function-predicate follow-up).
1148 if matches!(self.peek(), Token::LParen)
1149 && matches!(
1150 kind.as_str(),
1151 "function" | "procedure" | "aggregate" | "routine"
1152 )
1153 {
1154 let mut depth = 0usize;
1155 loop {
1156 match self.advance() {
1157 Token::LParen => depth += 1,
1158 Token::RParen => {
1159 depth -= 1;
1160 if depth == 0 {
1161 break;
1162 }
1163 }
1164 Token::Eof => {
1165 return Err(self.err(alloc::string::String::from(
1166 "unterminated argument list in COMMENT ON",
1167 )));
1168 }
1169 _ => {}
1170 }
1171 }
1172 }
1173 // `IS`
1174 if !matches!(self.peek(), Token::Is) {
1175 self.expect_keyword_ident("is")?;
1176 } else {
1177 self.advance();
1178 }
1179 let comment = match self.peek() {
1180 Token::Null => {
1181 self.advance();
1182 None
1183 }
1184 _ => Some(self.expect_string_literal()?),
1185 };
1186 Ok(Statement::CommentOn {
1187 kind,
1188 name,
1189 comment,
1190 })
1191 }
1192
1193 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1194 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1195 /// [CASCADE|RESTRICT]`.
1196 ///
1197 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1198 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1199 /// and the no-ON `GRANT role TO role` membership form — parses into
1200 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1201 /// on them still restores.
1202 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1203 self.advance(); // GRANT / REVOKE
1204 // REVOKE's optional `GRANT OPTION FOR` prefix.
1205 let mut grant_option = false;
1206 if !grant
1207 && self.peek_keyword_ident("grant")
1208 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1209 {
1210 self.advance(); // GRANT
1211 self.advance(); // OPTION
1212 self.expect_keyword_ident("for")?;
1213 grant_option = true;
1214 }
1215 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1216 // words each with an optional COLUMN list.
1217 let mut privileges: Vec<GrantPriv> = Vec::new();
1218 if matches!(self.peek(), Token::All) {
1219 self.advance();
1220 if self.peek_keyword_ident("privileges") {
1221 self.advance();
1222 }
1223 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1224 // column only.
1225 if matches!(self.peek(), Token::LParen) {
1226 let columns = self.parse_grant_column_list()?;
1227 privileges.push(GrantPriv {
1228 word: "ALL".into(),
1229 columns,
1230 });
1231 }
1232 } else {
1233 loop {
1234 // SELECT and INSERT lex as reserved tokens, so they never
1235 // reach `expect_ident_like` as plain idents; the rest
1236 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1237 // MAINTAIN) are ordinary identifiers.
1238 let w = match self.peek() {
1239 Token::Select => {
1240 self.advance();
1241 "SELECT".to_string()
1242 }
1243 Token::Insert => {
1244 self.advance();
1245 "INSERT".to_string()
1246 }
1247 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1248 // schema / database, and it lexes as a reserved token.
1249 Token::Create => {
1250 self.advance();
1251 "CREATE".to_string()
1252 }
1253 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1254 // alice`) these "privilege words" are ROLE NAMES, and a
1255 // role name is case-sensitive. `priv_from_word` folds case
1256 // itself when they really are privileges.
1257 _ => self.expect_ident_like()?,
1258 };
1259 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1260 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1261 let columns = if matches!(self.peek(), Token::LParen) {
1262 self.parse_grant_column_list()?
1263 } else {
1264 Vec::new()
1265 };
1266 privileges.push(GrantPriv { word: w, columns });
1267 if matches!(self.peek(), Token::Comma) {
1268 self.advance();
1269 } else {
1270 break;
1271 }
1272 }
1273 }
1274 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1275 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1276 if !matches!(self.peek(), Token::On) {
1277 let roles: Vec<String> = core::mem::take(&mut privileges)
1278 .into_iter()
1279 .map(|p| p.word)
1280 .collect();
1281 let grantees = self.parse_grantee_list(grant)?;
1282 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1283 // no admin-option layer: a member cannot re-grant).
1284 self.consume_until_statement_boundary();
1285 return Ok(finish_grant(
1286 grant,
1287 GrantStatement {
1288 privileges: Vec::new(),
1289 object: GrantObject::Roles(roles),
1290 grantees,
1291 grant_option,
1292 },
1293 ));
1294 }
1295 self.advance(); // ON
1296 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1297 // the enforced case; anything else parses and no-ops.
1298 let mut class = "TABLE";
1299 match self.peek() {
1300 Token::Table => {
1301 self.advance();
1302 }
1303 Token::All => {
1304 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1305 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1306 // IN SCHEMA` stay no-ops and keep their own object class.
1307 self.advance(); // ALL
1308 let kind = match self.peek() {
1309 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1310 // TABLES has its own token (SHOW TABLES owns it).
1311 Token::Tables | Token::Table => "tables".to_string(),
1312 _ => String::new(),
1313 };
1314 if !kind.is_empty() {
1315 self.advance();
1316 }
1317 // `IN SCHEMA <name>`
1318 if matches!(self.peek(), Token::In) {
1319 self.advance();
1320 if self.peek_keyword_ident("schema") {
1321 self.advance();
1322 let _schema = self.expect_ident_like()?;
1323 }
1324 }
1325 if kind != "tables" {
1326 self.consume_until_statement_boundary();
1327 return Ok(finish_grant(
1328 grant,
1329 GrantStatement {
1330 privileges,
1331 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1332 grantees: Vec::new(),
1333 grant_option,
1334 },
1335 ));
1336 }
1337 let grantees = self.parse_grantee_list(grant)?;
1338 self.consume_until_statement_boundary();
1339 return Ok(finish_grant(
1340 grant,
1341 GrantStatement {
1342 privileges,
1343 object: GrantObject::AllTablesInSchema,
1344 grantees,
1345 grant_option,
1346 },
1347 ));
1348 }
1349 Token::Ident(w) | Token::QuotedIdent(w) => {
1350 let lc = w.to_ascii_lowercase();
1351 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1352 // real objects with real ACLs now.
1353 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1354 self.advance();
1355 let mut names: Vec<String> = Vec::new();
1356 loop {
1357 let mut parts: Vec<String> = Vec::new();
1358 loop {
1359 parts.push(self.expect_ident_like()?);
1360 if matches!(self.peek(), Token::Dot) {
1361 self.advance();
1362 } else {
1363 break;
1364 }
1365 }
1366 names.push(parts.pop().expect("at least one part"));
1367 if matches!(self.peek(), Token::Comma) {
1368 self.advance();
1369 } else {
1370 break;
1371 }
1372 }
1373 let grantees = self.parse_grantee_list(grant)?;
1374 let mut grant_option = grant_option;
1375 if grant && self.peek_keyword_ident("with") {
1376 self.advance();
1377 self.expect_keyword_ident("grant")?;
1378 self.expect_keyword_ident("option")?;
1379 grant_option = true;
1380 }
1381 self.consume_until_statement_boundary();
1382 let object = match lc.as_str() {
1383 "sequence" => GrantObject::Sequences(names),
1384 "schema" => GrantObject::Schemas(names),
1385 _ => GrantObject::Databases(names),
1386 };
1387 return Ok(finish_grant(
1388 grant,
1389 GrantStatement {
1390 privileges,
1391 object,
1392 grantees,
1393 grant_option,
1394 },
1395 ));
1396 }
1397 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1398 // keys functions by NAME, so the argument list parses and is
1399 // dropped (an overload set shares one ACL — recorded residual).
1400 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1401 self.advance();
1402 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1403 loop {
1404 let mut parts: Vec<String> = Vec::new();
1405 loop {
1406 parts.push(self.expect_ident_like()?);
1407 if matches!(self.peek(), Token::Dot) {
1408 self.advance();
1409 } else {
1410 break;
1411 }
1412 }
1413 let fname = parts.pop().expect("at least one part");
1414 // v7.39 (read01 round 62) — the signature picks the
1415 // overload, so it is captured.
1416 let sig = if matches!(self.peek(), Token::LParen) {
1417 Some(self.parse_function_signature_types()?)
1418 } else {
1419 None
1420 };
1421 names.push((fname, sig));
1422 if matches!(self.peek(), Token::Comma) {
1423 self.advance();
1424 } else {
1425 break;
1426 }
1427 }
1428 let grantees = self.parse_grantee_list(grant)?;
1429 self.consume_until_statement_boundary();
1430 return Ok(finish_grant(
1431 grant,
1432 GrantStatement {
1433 privileges,
1434 object: GrantObject::Functions(names),
1435 grantees,
1436 grant_option,
1437 },
1438 ));
1439 }
1440 if matches!(
1441 lc.as_str(),
1442 "type"
1443 | "domain"
1444 | "language"
1445 | "tablespace"
1446 | "large"
1447 | "foreign"
1448 | "parameter"
1449 ) {
1450 self.consume_until_statement_boundary();
1451 return Ok(finish_grant(
1452 grant,
1453 GrantStatement {
1454 privileges,
1455 object: GrantObject::Other(lc.to_ascii_uppercase()),
1456 grantees: Vec::new(),
1457 grant_option,
1458 },
1459 ));
1460 }
1461 class = "TABLE";
1462 }
1463 _ => {}
1464 }
1465 let _ = class;
1466 // The table list. Schema-qualified names drop their qualifier (SPG is
1467 // single-schema) — but read the dotted parts from raw tokens, since
1468 // `expect_ident_like` would silently swallow the leading part.
1469 let mut tables: Vec<String> = Vec::new();
1470 loop {
1471 let mut parts: Vec<String> = Vec::new();
1472 loop {
1473 parts.push(self.expect_ident_like()?);
1474 if matches!(self.peek(), Token::Dot) {
1475 self.advance();
1476 } else {
1477 break;
1478 }
1479 }
1480 tables.push(parts.pop().expect("at least one part"));
1481 if matches!(self.peek(), Token::Comma) {
1482 self.advance();
1483 } else {
1484 break;
1485 }
1486 }
1487 let grantees = self.parse_grantee_list(grant)?;
1488 if grant && self.peek_keyword_ident("with") {
1489 self.advance();
1490 self.expect_keyword_ident("grant")?;
1491 self.expect_keyword_ident("option")?;
1492 grant_option = true;
1493 }
1494 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1495 // to cascade to (no re-granting), so both are accepted and ignored.
1496 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1497 self.advance();
1498 }
1499 Ok(finish_grant(
1500 grant,
1501 GrantStatement {
1502 privileges,
1503 object: GrantObject::Tables(tables),
1504 grantees,
1505 grant_option,
1506 },
1507 ))
1508 }
1509
1510 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1511 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1512 /// words; the caller normalises them into a signature key.
1513 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1514 self.advance(); // (
1515 let mut types: Vec<String> = Vec::new();
1516 if matches!(self.peek(), Token::RParen) {
1517 self.advance();
1518 return Ok(types);
1519 }
1520 loop {
1521 // Collect the words of one argument up to a comma / close paren.
1522 let mut words: Vec<String> = Vec::new();
1523 loop {
1524 match self.peek() {
1525 Token::Comma | Token::RParen | Token::Eof => break,
1526 _ => {}
1527 }
1528 let tok = self.advance();
1529 match tok {
1530 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1531 other => {
1532 if let Some(w) = unreserved_keyword_text(&other) {
1533 words.push(w);
1534 }
1535 }
1536 }
1537 }
1538 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1539 // themselves several words (`double precision`, `character
1540 // varying`, `timestamp with time zone`), so "two words means the
1541 // first is a parameter name" reads the type off `f(double
1542 // precision)` as `precision`. v7.39 (round 282): recognise the
1543 // multi-word spellings first — a leading word that STARTS one of
1544 // them is part of the type, not a name.
1545 let joined = words.join(" ");
1546 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1547 joined
1548 } else if words.len() >= 2 {
1549 words[1..].join(" ")
1550 } else {
1551 words.first().cloned().unwrap_or_default()
1552 };
1553 types.push(ty);
1554 if matches!(self.peek(), Token::Comma) {
1555 self.advance();
1556 } else {
1557 break;
1558 }
1559 }
1560 if matches!(self.peek(), Token::RParen) {
1561 self.advance();
1562 }
1563 Ok(types)
1564 }
1565
1566 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1567 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1568 self.advance(); // (
1569 let mut cols = Vec::new();
1570 loop {
1571 cols.push(self.expect_ident_like()?);
1572 if matches!(self.peek(), Token::Comma) {
1573 self.advance();
1574 } else {
1575 break;
1576 }
1577 }
1578 if !matches!(self.peek(), Token::RParen) {
1579 return Err(self.err(alloc::format!(
1580 "expected ')' to close the column list, got {:?}",
1581 self.peek()
1582 )));
1583 }
1584 self.advance(); // )
1585 Ok(cols)
1586 }
1587
1588 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1589 /// PUBLIC.
1590 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1591 if grant {
1592 if matches!(self.peek(), Token::To) {
1593 self.advance();
1594 } else {
1595 self.expect_keyword_ident("to")?;
1596 }
1597 } else if matches!(self.peek(), Token::From) {
1598 self.advance();
1599 } else {
1600 self.expect_keyword_ident("from")?;
1601 }
1602 let mut grantees: Vec<String> = Vec::new();
1603 loop {
1604 // `GROUP name` is the legacy spelling of a plain role name.
1605 if self.peek_keyword_ident("group") {
1606 self.advance();
1607 }
1608 if self.peek_keyword_ident("public") {
1609 self.advance();
1610 grantees.push(String::new()); // PUBLIC
1611 } else {
1612 grantees.push(self.expect_ident_like()?);
1613 }
1614 if matches!(self.peek(), Token::Comma) {
1615 self.advance();
1616 } else {
1617 break;
1618 }
1619 }
1620 Ok(grantees)
1621 }
1622
1623 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1624 /// The body keeps its `$N` placeholders; substitution happens at
1625 /// EXECUTE. The declared types are recorded for
1626 /// `pg_prepared_statements.parameter_types` but are not enforced —
1627 /// PG infers when the list is omitted, and SPG resolves the values
1628 /// at substitution time either way.
1629 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1630 let start = self.pos;
1631 self.advance(); // PREPARE
1632 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1633 // different statement that happens to share the keyword. PG
1634 // ships with `max_prepared_transactions = 0` and reports it
1635 // this way; SPG has no prepared-transaction registry, so the
1636 // same wording is the accurate answer rather than a dodge.
1637 // Round 277 turned this from a silent no-op into a confusing
1638 // "expected AS in PREPARE" parse error.
1639 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1640 self.advance();
1641 let gid = match self.advance() {
1642 Token::String(g) => g,
1643 other => {
1644 return Err(self.err(alloc::format!(
1645 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1646 )));
1647 }
1648 };
1649 return Ok(Statement::PrepareTransaction(gid));
1650 }
1651 let name = self.expect_ident_like()?;
1652 let mut param_types = Vec::new();
1653 if matches!(self.peek(), Token::LParen) {
1654 self.advance();
1655 loop {
1656 let mut ty = self.expect_ident_like()?;
1657 // A parameterised type name (`numeric(10,2)`,
1658 // `varchar(20)`) keeps its argument list in the text.
1659 if matches!(self.peek(), Token::LParen) {
1660 let mut depth = 0usize;
1661 let mut buf = String::from("(");
1662 loop {
1663 match self.advance() {
1664 Token::LParen => {
1665 depth += 1;
1666 if depth > 1 {
1667 buf.push('(');
1668 }
1669 }
1670 Token::RParen => {
1671 depth -= 1;
1672 buf.push(')');
1673 if depth == 0 {
1674 break;
1675 }
1676 }
1677 Token::Comma => buf.push(','),
1678 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1679 Token::Eof => break,
1680 _ => {}
1681 }
1682 }
1683 ty.push_str(&buf);
1684 }
1685 param_types.push(ty);
1686 match self.peek() {
1687 Token::Comma => {
1688 self.advance();
1689 }
1690 Token::RParen => {
1691 self.advance();
1692 break;
1693 }
1694 other => {
1695 return Err(self.err(alloc::format!(
1696 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1697 )));
1698 }
1699 }
1700 }
1701 }
1702 if !matches!(self.peek(), Token::As) {
1703 return Err(self.err(alloc::format!(
1704 "expected AS in PREPARE, got {:?}",
1705 self.peek()
1706 )));
1707 }
1708 self.advance();
1709 let body = self.parse_one_statement()?;
1710 // The Parser holds tokens, not the source text, so the
1711 // statement PG reports in `pg_prepared_statements.statement`
1712 // is rebuilt from the AST rather than sliced from the input.
1713 let _ = start;
1714 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1715 if !param_types.is_empty() {
1716 source.push_str(" (");
1717 source.push_str(¶m_types.join(", "));
1718 source.push(')');
1719 }
1720 source.push_str(" AS ");
1721 source.push_str(&alloc::format!("{body}"));
1722 Ok(Statement::Prepare {
1723 name,
1724 param_types,
1725 body: alloc::boxed::Box::new(body),
1726 source,
1727 })
1728 }
1729
1730 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1731 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1732 self.advance(); // EXECUTE
1733 let name = self.expect_ident_like()?;
1734 let mut args = Vec::new();
1735 if matches!(self.peek(), Token::LParen) {
1736 self.advance();
1737 if matches!(self.peek(), Token::RParen) {
1738 self.advance();
1739 } else {
1740 loop {
1741 args.push(self.parse_expr(0)?);
1742 match self.advance() {
1743 Token::Comma => {}
1744 Token::RParen => break,
1745 other => {
1746 return Err(self.err(alloc::format!(
1747 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1748 )));
1749 }
1750 }
1751 }
1752 }
1753 }
1754 Ok(Statement::Execute { name, args })
1755 }
1756
1757 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1758 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1759 /// procedure catalog yet, so this reports PG's not-found error
1760 /// (with its HINT) rather than pretending the call ran.
1761 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1762 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1763 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1764 self.advance(); // DISCARD
1765 let target = match self.advance() {
1766 Token::All => DiscardTarget::All,
1767 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1768 "all" => DiscardTarget::All,
1769 "plans" => DiscardTarget::Plans,
1770 "sequences" => DiscardTarget::Sequences,
1771 "temp" | "temporary" => DiscardTarget::Temp,
1772 other => {
1773 return Err(self.err(format!(
1774 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1775 )));
1776 }
1777 },
1778 other => {
1779 return Err(self.err(format!(
1780 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1781 )));
1782 }
1783 };
1784 Ok(Statement::Discard(target))
1785 }
1786
1787 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1788 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1789 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1790 /// aggressively the server interrupts, which SPG does not distinguish.
1791 /// Bare `KILL <id>` means CONNECTION.
1792 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1793 self.advance(); // KILL
1794 let mut query_only = false;
1795 loop {
1796 // CONNECTION is a reserved keyword token (it also opens
1797 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1798 // `Token::Connection` rather than a bare ident.
1799 if matches!(self.peek(), Token::Connection) {
1800 self.advance();
1801 break;
1802 }
1803 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1804 break;
1805 };
1806 match w.to_ascii_lowercase().as_str() {
1807 "hard" | "soft" => {
1808 self.advance();
1809 }
1810 "query" => {
1811 self.advance();
1812 query_only = true;
1813 break;
1814 }
1815 _ => break,
1816 }
1817 }
1818 let id = self.parse_expr(0)?;
1819 Ok(Statement::Kill {
1820 query_only,
1821 id: Box::new(id),
1822 })
1823 }
1824
1825 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1826 self.advance(); // CALL
1827 let name = self.expect_ident_like()?;
1828 self.consume_until_statement_boundary();
1829 Ok(Statement::Call(name))
1830 }
1831
1832 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1833 self.advance(); // DEALLOCATE
1834 // PG accepts an optional noise `PREPARE` keyword here.
1835 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1836 self.advance();
1837 }
1838 if matches!(self.peek(), Token::All) {
1839 self.advance();
1840 return Ok(Statement::Deallocate(None));
1841 }
1842 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1843 self.advance();
1844 return Ok(Statement::Deallocate(None));
1845 }
1846 let name = self.expect_ident_like()?;
1847 Ok(Statement::Deallocate(Some(name)))
1848 }
1849
1850 fn consume_until_statement_boundary(&mut self) {
1851 loop {
1852 match self.peek() {
1853 Token::Semicolon | Token::Eof => return,
1854 _ => self.advance(),
1855 };
1856 }
1857 }
1858
1859 /// v7.22 (round-13 T2) — consume to the statement boundary like
1860 /// `consume_until_statement_boundary`, but pick out the sequence
1861 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1862 /// columns) or the first string literal (`nextval('<seq>')`).
1863 /// Schema qualifiers and `::regclass` casts are stripped.
1864 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1865 let mut seq: Option<String> = None;
1866 let mut after_sequence_kw = false;
1867 let mut after_name_kw = false;
1868 loop {
1869 match self.peek().clone() {
1870 Token::Semicolon | Token::Eof => break,
1871 Token::Ident(s) | Token::QuotedIdent(s) => {
1872 if after_name_kw && seq.is_none() {
1873 self.advance();
1874 let mut name = s;
1875 // `SEQUENCE NAME public.groups_id_seq` — keep
1876 // the bare name, drop qualifiers.
1877 while matches!(self.peek(), Token::Dot) {
1878 self.advance();
1879 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1880 name = n;
1881 }
1882 }
1883 seq = Some(name);
1884 after_name_kw = false;
1885 continue;
1886 }
1887 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1888 after_name_kw = true;
1889 after_sequence_kw = false;
1890 } else {
1891 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1892 }
1893 self.advance();
1894 }
1895 Token::String(s) => {
1896 if seq.is_none() {
1897 // `nextval('public.groups_id_seq'::regclass)`
1898 let bare = s
1899 .rsplit_once('.')
1900 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1901 seq = Some(bare);
1902 }
1903 self.advance();
1904 }
1905 _ => {
1906 after_sequence_kw = false;
1907 after_name_kw = false;
1908 self.advance();
1909 }
1910 }
1911 }
1912 seq
1913 }
1914
1915 /// v7.39 (round 621) — is the next token the keyword `BY`?
1916 ///
1917 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1918 /// column, table and alias name — and SPG lexed it into a dedicated
1919 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1920 /// two-letter keywords the lexer knew, this was the only one PG leaves
1921 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1922 ///
1923 /// The token is gone; the three clauses that own the word — GROUP BY,
1924 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1925 /// ask this instead. Adding it to the unreserved-identifier table was not
1926 /// enough on its own: identifier positions that match the token shape
1927 /// directly (an index's column list, a table alias) never consult that
1928 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1929 /// Not lexing it as a keyword closes the whole class rather than the two
1930 /// positions that happened to be noticed.
1931 fn peek_is_by(&self) -> bool {
1932 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1933 }
1934
1935 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1936 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1937 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1938 fn consume_drop_behaviour(&mut self) {
1939 if matches!(
1940 self.peek(),
1941 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1942 ) {
1943 self.advance();
1944 }
1945 }
1946
1947 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1948 let first = match self.advance() {
1949 Token::Ident(s) | Token::QuotedIdent(s) => s,
1950 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1951 // per PG's `pg_get_keywords()` classification. SPG tokenizes
1952 // these as named variants for parsing leverage in the
1953 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1954 // `BEGIN`, etc.), but they MUST still be usable as table /
1955 // column / alias names in DDL+DML. Sentori migrations like
1956 // 0001_init.sql ship `release TEXT NOT NULL` in the events
1957 // table — the `events.release` column carries the release
1958 // identifier string. Pre-T4 this triggered "expected
1959 // identifier, got Release" and blocked every drop-in user
1960 // whose schema had a column / alias with one of these names.
1961 other if unreserved_keyword_text(&other).is_some() => {
1962 unreserved_keyword_text(&other).unwrap()
1963 }
1964 other => {
1965 return Err(ParseError {
1966 message: format!("expected identifier, got {other:?}"),
1967 token_pos: self.consumed_pos(),
1968 });
1969 }
1970 };
1971 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1972 // qualify every name with `public.` (and pg_catalog.* for
1973 // functions); SPG is single-schema so we discard the
1974 // prefix and return only the trailing ident. Same shape
1975 // also handles MySQL `db.tbl` cross-database refs (SPG
1976 // ignores the db part).
1977 if matches!(self.peek(), Token::Dot) {
1978 self.advance();
1979 match self.advance() {
1980 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1981 other if unreserved_keyword_text(&other).is_some() => {
1982 return Ok(unreserved_keyword_text(&other).unwrap());
1983 }
1984 other => {
1985 return Err(ParseError {
1986 message: format!("expected identifier after '{first}.', got {other:?}"),
1987 token_pos: self.consumed_pos(),
1988 });
1989 }
1990 }
1991 }
1992 Ok(first)
1993 }
1994
1995 #[allow(clippy::too_many_lines)]
1996 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
1997 // v7.14.0 — empty / comment-only / semicolon-only input
1998 // (after the lexer strips line + block + MySQL
1999 // conditional comments) lands as Statement::Empty.
2000 // pg_dump and mysqldump emit several wrappers that
2001 // collapse to nothing after stripping (`/*!40101 SET …
2002 // */;`, blank lines between statements); the engine
2003 // returns CommandOk no-op so the dump loads cleanly.
2004 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2005 return Ok(Statement::Empty);
2006 }
2007 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2008 // catalog / metadata DDL that has no behavioural effect
2009 // on SPG's single-schema, single-database, single-user
2010 // model. Consume the whole statement up to the next
2011 // semicolon / EOF and return Empty. This is broader than
2012 // the per-keyword DROP / SET / COMMENT arms but lets the
2013 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2014 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2015 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2016 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2017 let lc = s.to_ascii_lowercase();
2018 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2019 if lc == "comment" {
2020 return self.parse_comment_on();
2021 }
2022 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2023 if lc == "grant" || lc == "revoke" {
2024 return self.parse_grant_or_revoke(lc == "grant");
2025 }
2026 // v7.39 (round 277) — the SQL-level prepared-statement
2027 // surface is REAL now. It used to be accepted and dropped
2028 // on the theory that "real execution still happens via the
2029 // extended-query flow" — true only for a driver that uses
2030 // that flow; a plain SQL PREPARE / EXECUTE returned no
2031 // rows at all.
2032 if lc == "prepare" {
2033 return self.parse_prepare();
2034 }
2035 if lc == "execute" {
2036 return self.parse_execute();
2037 }
2038 if lc == "deallocate" {
2039 return self.parse_deallocate();
2040 }
2041 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2042 // accepted and dropped, so an application's stored-procedure
2043 // invocation reported success and did nothing. SPG has no
2044 // procedure catalog, so every CALL names a procedure that
2045 // does not exist — which is exactly what PG says.
2046 if lc == "call" {
2047 return self.parse_call();
2048 }
2049 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2050 // names one connection and acts on it.
2051 if lc == "kill" {
2052 return self.parse_kill();
2053 }
2054 if lc == "discard" {
2055 return self.parse_discard();
2056 }
2057 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2058 // Still performs nothing; the roles are carried out so a name
2059 // that does not exist is refused, as PG18 refuses it.
2060 if lc == "reassign" {
2061 self.advance();
2062 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2063 self.advance();
2064 }
2065 if self.peek_is_by() {
2066 self.advance();
2067 }
2068 // Only the roles BEFORE the TO are the ones that must
2069 // exist — `TO` names the new owner, which PG checks as
2070 // well, so both lists are collected.
2071 let mut names = self.take_comma_separated_names();
2072 if matches!(self.peek(), Token::To) {
2073 self.advance();
2074 names.extend(self.take_comma_separated_names());
2075 }
2076 self.consume_until_statement_boundary();
2077 return Ok(Statement::ValidateOnly {
2078 kind: crate::ast::ValidateOnlyKind::RoleName,
2079 names,
2080 });
2081 }
2082 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2083 // unconditionally with `no security label providers have been
2084 // loaded`, whatever object it names, because none is loaded.
2085 // SPG has none either; accepting it told the caller a label had
2086 // been applied when nothing anywhere records one.
2087 if lc == "security" {
2088 self.consume_until_statement_boundary();
2089 return Ok(Statement::ValidateOnly {
2090 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2091 names: Vec::new(),
2092 });
2093 }
2094 if is_dump_noise_statement(&lc) {
2095 self.consume_until_statement_boundary();
2096 return Ok(Statement::Empty);
2097 }
2098 }
2099 match self.peek() {
2100 Token::Select => self.parse_select_stmt(),
2101 // v7.37.17 (17.6 siblings) — a statement opening with a
2102 // parenthesized query group: `(SELECT … UNION …)
2103 // INTERSECT …`. parse_bare_select's group arm consumes
2104 // the parens; the select parser handles the outer chain
2105 // and tail.
2106 Token::LParen
2107 if matches!(
2108 self.tokens.get(self.pos + 1),
2109 Some(Token::Select | Token::LParen | Token::Values)
2110 ) =>
2111 {
2112 self.parse_select_stmt()
2113 }
2114 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2115 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2116 // Lowers to the same UNION ALL chain the FROM-position
2117 // form uses, then reuses the shared SELECT tail.
2118 Token::Values => {
2119 self.advance(); // VALUES
2120 let mut head = self.parse_values_rows_body()?;
2121 self.parse_select_tail_into(&mut head)?;
2122 Ok(Statement::Select(head))
2123 }
2124 // SQL-standard `TABLE name` shorthand for
2125 // `SELECT * FROM name` — pg_dump never emits it, but
2126 // psql users and PG docs use it constantly. Set-op
2127 // chains and the ORDER BY/LIMIT tail compose like any
2128 // SELECT head.
2129 Token::Table
2130 if matches!(
2131 self.tokens.get(self.pos + 1),
2132 Some(Token::Ident(_) | Token::QuotedIdent(_))
2133 ) =>
2134 {
2135 let mut head = self.parse_table_shorthand()?;
2136 self.parse_setop_chain_into(&mut head)?;
2137 self.parse_select_tail_into(&mut head)?;
2138 Ok(Statement::Select(head))
2139 }
2140 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2141 // body is a dollar-quoted plpgsql block (lexer already
2142 // collapsed `$$…$$` into a single Token::String).
2143 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2144 // real PlPgSqlBlock so the engine can EXECUTE it at
2145 // top level instead of silently swallowing. Pre-
2146 // v7.16.2 the parser threw the body away and the
2147 // engine returned CommandOk for the entire DO; that
2148 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2149 // $$` into a SEV-1 silent no-op (the IF + the rename
2150 // were both invisible — mailrs's migrate-042 didn't
2151 // actually run). Now the body parses + executes;
2152 // EmbeddedSql inside the block runs immediately
2153 // against the engine (not deferred — we're at top
2154 // level, not inside a trigger row-write loop).
2155 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2156 self.advance();
2157 let body_text = match self.advance() {
2158 Token::String(s) => s,
2159 other => {
2160 return Err(self.err(alloc::format!(
2161 "expected dollar-quoted body after DO, got {other:?}"
2162 )));
2163 }
2164 };
2165 // Optional `LANGUAGE <name>` trailer (idents only).
2166 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2167 self.advance();
2168 let _ = self.expect_ident_like()?;
2169 }
2170 // Parse the body — same shape CREATE FUNCTION
2171 // uses for trigger function bodies. If the body
2172 // doesn't parse cleanly we surface the error
2173 // (better than silent no-op).
2174 let block = parse_plpgsql_body(&body_text)?;
2175 Ok(Statement::DoBlock(block))
2176 }
2177 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2178 // WITH isn't a reserved token in our lexer — comes through
2179 // as `Token::Ident("with")` (case-insensitive).
2180 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2181 self.advance();
2182 self.parse_with_cte_then_select()
2183 }
2184 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2185 // an identifier — not a reserved keyword.
2186 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2187 self.advance();
2188 let mut analyze = false;
2189 let mut suggest = false;
2190 let mut costs_off = false;
2191 let mut buffers = false;
2192 let mut timing_off = false;
2193 let mut settings = false;
2194 let mut wal = false;
2195 let mut summary_off = false;
2196 let mut format = crate::ast::ExplainFormat::Text;
2197 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2198 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2199 // options are comma-separated. Booleans default to ON
2200 // when the value token is omitted (matches PG).
2201 if matches!(self.peek(), Token::LParen) {
2202 self.advance();
2203 loop {
2204 let opt = match self.peek().clone() {
2205 Token::Ident(s) | Token::QuotedIdent(s) => s,
2206 other => {
2207 return Err(self.err(format!(
2208 "expected option keyword inside EXPLAIN (…), got {other:?}"
2209 )));
2210 }
2211 };
2212 self.advance();
2213 if opt.eq_ignore_ascii_case("suggest") {
2214 suggest = true;
2215 // SUGGEST takes no explicit value today.
2216 } else if opt.eq_ignore_ascii_case("costs") {
2217 // PG syntax: `COSTS [ON | OFF]`. Default
2218 // when value omitted is ON, so plain
2219 // `COSTS` is a no-op. `COSTS OFF` flips.
2220 // `ON` lexes to `Token::On` (reserved
2221 // keyword in JOIN ... ON contexts); accept
2222 // it alongside the bare Ident form so the
2223 // grammar matches PG verbatim.
2224 let value = match self.peek().clone() {
2225 Token::On => {
2226 self.advance();
2227 true
2228 }
2229 Token::Ident(v) | Token::QuotedIdent(v)
2230 if v.eq_ignore_ascii_case("off") =>
2231 {
2232 self.advance();
2233 false
2234 }
2235 Token::Ident(v) | Token::QuotedIdent(v)
2236 if v.eq_ignore_ascii_case("true") =>
2237 {
2238 self.advance();
2239 true
2240 }
2241 _ => true,
2242 };
2243 costs_off = !value;
2244 } else if opt.eq_ignore_ascii_case("analyze")
2245 || opt.eq_ignore_ascii_case("analyse")
2246 {
2247 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2248 // Same default-ON rule as ANALYZE keyword form.
2249 let value = match self.peek().clone() {
2250 Token::On => {
2251 self.advance();
2252 true
2253 }
2254 Token::Ident(v) | Token::QuotedIdent(v)
2255 if v.eq_ignore_ascii_case("off") =>
2256 {
2257 self.advance();
2258 false
2259 }
2260 Token::Ident(v) | Token::QuotedIdent(v)
2261 if v.eq_ignore_ascii_case("true") =>
2262 {
2263 self.advance();
2264 true
2265 }
2266 _ => true,
2267 };
2268 analyze = value;
2269 } else if opt.eq_ignore_ascii_case("buffers") {
2270 // v7.37.22 — `BUFFERS [ON|OFF]`.
2271 let value = match self.peek().clone() {
2272 Token::On => {
2273 self.advance();
2274 true
2275 }
2276 Token::Ident(v) | Token::QuotedIdent(v)
2277 if v.eq_ignore_ascii_case("off") =>
2278 {
2279 self.advance();
2280 false
2281 }
2282 Token::Ident(v) | Token::QuotedIdent(v)
2283 if v.eq_ignore_ascii_case("true") =>
2284 {
2285 self.advance();
2286 true
2287 }
2288 _ => true,
2289 };
2290 buffers = value;
2291 } else if opt.eq_ignore_ascii_case("timing") {
2292 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2293 // the measured wall-clock annotation.
2294 let value = match self.peek().clone() {
2295 Token::On => {
2296 self.advance();
2297 true
2298 }
2299 Token::Ident(v) | Token::QuotedIdent(v)
2300 if v.eq_ignore_ascii_case("off") =>
2301 {
2302 self.advance();
2303 false
2304 }
2305 Token::Ident(v) | Token::QuotedIdent(v)
2306 if v.eq_ignore_ascii_case("true") =>
2307 {
2308 self.advance();
2309 true
2310 }
2311 _ => true,
2312 };
2313 timing_off = !value;
2314 } else if opt.eq_ignore_ascii_case("settings") {
2315 settings = true;
2316 } else if opt.eq_ignore_ascii_case("wal") {
2317 wal = true;
2318 } else if opt.eq_ignore_ascii_case("summary") {
2319 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2320 // gates the trailing Planning/Execution Time
2321 // lines now (was accept-and-no-op).
2322 let value = match self.peek().clone() {
2323 Token::On => {
2324 self.advance();
2325 true
2326 }
2327 Token::Ident(v) | Token::QuotedIdent(v)
2328 if v.eq_ignore_ascii_case("off") =>
2329 {
2330 self.advance();
2331 false
2332 }
2333 Token::Ident(v) | Token::QuotedIdent(v)
2334 if v.eq_ignore_ascii_case("true") =>
2335 {
2336 self.advance();
2337 true
2338 }
2339 _ => true,
2340 };
2341 summary_off = !value;
2342 } else if opt.eq_ignore_ascii_case("verbose")
2343 || opt.eq_ignore_ascii_case("format")
2344 {
2345 // v7.37.22 — accept-but-no-op the remaining
2346 // PG options so EXPLAIN-using clients
2347 // (pgAdmin / DataGrip) don't see syntax
2348 // errors. FORMAT takes a value (text /
2349 // json / yaml / xml); skip the next token
2350 // if it's an ident.
2351 if opt.eq_ignore_ascii_case("format") {
2352 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2353 {
2354 self.advance();
2355 format = match v.to_ascii_lowercase().as_str() {
2356 "text" => crate::ast::ExplainFormat::Text,
2357 "json" => crate::ast::ExplainFormat::Json,
2358 "xml" => crate::ast::ExplainFormat::Xml,
2359 "yaml" => crate::ast::ExplainFormat::Yaml,
2360 other => {
2361 return Err(self.err(format!(
2362 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2363 supports text, json, xml, yaml"
2364 )));
2365 }
2366 };
2367 }
2368 } else {
2369 // VERBOSE / SUMMARY take optional ON/OFF;
2370 // consume if present.
2371 if matches!(self.peek(), Token::On) {
2372 self.advance();
2373 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2374 self.peek().clone()
2375 && (v.eq_ignore_ascii_case("off")
2376 || v.eq_ignore_ascii_case("true"))
2377 {
2378 self.advance();
2379 let _ = v;
2380 }
2381 }
2382 } else {
2383 return Err(self.err(format!(
2384 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2385 )));
2386 }
2387 if matches!(self.peek(), Token::Comma) {
2388 self.advance();
2389 continue;
2390 }
2391 break;
2392 }
2393 if !matches!(self.peek(), Token::RParen) {
2394 return Err(self.err(format!(
2395 "expected ')' after EXPLAIN options, got {:?}",
2396 self.peek()
2397 )));
2398 }
2399 self.advance();
2400 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2401 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2402 {
2403 self.advance();
2404 analyze = true;
2405 }
2406 // v7.39 (round 224) — the body may open with WITH (CTEs);
2407 // route through the same CTE-then-SELECT path the top-level
2408 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2409 // too (PG explains INSERT / UPDATE / DELETE).
2410 let inner = match self.peek().clone() {
2411 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2412 self.advance();
2413 self.parse_with_cte_then_select()?
2414 }
2415 Token::Insert => self.parse_insert_stmt(false)?,
2416 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2417 self.advance();
2418 self.parse_update_after_keyword()?
2419 }
2420 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2421 self.advance();
2422 self.parse_delete_after_keyword()?
2423 }
2424 _ => self.parse_select_stmt()?,
2425 };
2426 if !matches!(
2427 inner,
2428 Statement::Select(_)
2429 | Statement::Insert(_)
2430 | Statement::Update(_)
2431 | Statement::Delete(_)
2432 ) {
2433 return Err(self.err(format!(
2434 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2435 )));
2436 }
2437 Ok(Statement::Explain(crate::ast::ExplainStatement {
2438 analyze,
2439 inner: Box::new(inner),
2440 suggest,
2441 costs_off,
2442 buffers,
2443 timing_off,
2444 settings,
2445 wal,
2446 summary_off,
2447 format,
2448 }))
2449 }
2450 Token::Create => self.parse_create_stmt(),
2451 Token::Insert => self.parse_insert_stmt(false),
2452 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2453 // spelling; route to the same handler. DESC is the
2454 // reserved ORDER BY token, so it gets its own arm.
2455 Token::Ident(s)
2456 if s.eq_ignore_ascii_case("describe")
2457 && matches!(
2458 self.tokens.get(self.pos + 1),
2459 Some(Token::Ident(_) | Token::QuotedIdent(_))
2460 ) =>
2461 {
2462 self.advance();
2463 let table = self.expect_ident_like()?;
2464 Ok(Statement::ShowColumns(table))
2465 }
2466 Token::Desc
2467 if matches!(
2468 self.tokens.get(self.pos + 1),
2469 Some(Token::Ident(_) | Token::QuotedIdent(_))
2470 ) =>
2471 {
2472 self.advance();
2473 let table = self.expect_ident_like()?;
2474 Ok(Statement::ShowColumns(table))
2475 }
2476 // `COPY table [(cols)] TO STDOUT` — the export half of
2477 // pg_dump's COPY pair (the FROM stdin half rides the
2478 // embed import path). Options need a format design and
2479 // error honestly.
2480 Token::Ident(s)
2481 if s.eq_ignore_ascii_case("copy")
2482 && matches!(
2483 self.tokens.get(self.pos + 1),
2484 Some(Token::Ident(_) | Token::QuotedIdent(_))
2485 ) =>
2486 {
2487 self.advance(); // COPY
2488 let table = self.expect_ident_like()?;
2489 let columns = if matches!(self.peek(), Token::LParen) {
2490 self.advance();
2491 let mut cols = alloc::vec![self.expect_ident_like()?];
2492 while matches!(self.peek(), Token::Comma) {
2493 self.advance();
2494 cols.push(self.expect_ident_like()?);
2495 }
2496 if !matches!(self.peek(), Token::RParen) {
2497 return Err(self.err(format!(
2498 "expected ')' after COPY column list, got {:?}",
2499 self.peek()
2500 )));
2501 }
2502 self.advance();
2503 Some(cols)
2504 } else {
2505 None
2506 };
2507 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2508 // endpoint. (FROM STDIN still rides the wire/import path —
2509 // its data arrives out of band.)
2510 if matches!(self.peek(), Token::From)
2511 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2512 {
2513 self.advance(); // FROM
2514 let Token::String(path) = self.advance() else {
2515 unreachable!()
2516 };
2517 let options = self.parse_copy_to_options()?;
2518 return Ok(Statement::CopyFromFile {
2519 table,
2520 columns,
2521 path,
2522 options,
2523 });
2524 }
2525 if !matches!(self.peek(), Token::To) {
2526 return Err(self.err(format!(
2527 "COPY: only TO STDOUT is supported here (FROM stdin \
2528 rides the import path); got {:?}",
2529 self.peek()
2530 )));
2531 }
2532 self.advance();
2533 if matches!(self.peek(), Token::String(_)) {
2534 let Token::String(path) = self.advance() else { unreachable!() };
2535 let options = self.parse_copy_to_options()?;
2536 return Ok(Statement::CopyToFile {
2537 table,
2538 columns,
2539 query: None,
2540 path,
2541 options,
2542 });
2543 }
2544 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2545 return Err(self.err(format!(
2546 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2547 self.peek()
2548 )));
2549 }
2550 self.advance();
2551 let options = self.parse_copy_to_options()?;
2552 Ok(Statement::CopyTo {
2553 table,
2554 columns,
2555 query: None,
2556 options,
2557 })
2558 }
2559 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2560 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2561 // result set is streamed in COPY format (PG's query form).
2562 Token::Ident(s)
2563 if s.eq_ignore_ascii_case("copy")
2564 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2565 {
2566 self.advance(); // COPY
2567 self.advance(); // (
2568 let query = self.parse_select_stmt()?;
2569 if !matches!(self.peek(), Token::RParen) {
2570 return Err(self.err(format!(
2571 "expected ')' after COPY query, got {:?}",
2572 self.peek()
2573 )));
2574 }
2575 self.advance(); // )
2576 if !matches!(self.peek(), Token::To) {
2577 return Err(self.err(format!(
2578 "COPY (query): only TO STDOUT is supported, got {:?}",
2579 self.peek()
2580 )));
2581 }
2582 self.advance();
2583 if matches!(self.peek(), Token::String(_)) {
2584 let Token::String(path) = self.advance() else { unreachable!() };
2585 let options = self.parse_copy_to_options()?;
2586 return Ok(Statement::CopyToFile {
2587 table: String::new(),
2588 columns: None,
2589 query: Some(alloc::boxed::Box::new(query)),
2590 path,
2591 options,
2592 });
2593 }
2594 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2595 return Err(self.err(format!(
2596 "COPY (query): TO supports STDOUT only, got {:?}",
2597 self.peek()
2598 )));
2599 }
2600 self.advance();
2601 let options = self.parse_copy_to_options()?;
2602 Ok(Statement::CopyTo {
2603 table: String::new(),
2604 columns: None,
2605 query: Some(alloc::boxed::Box::new(query)),
2606 options,
2607 })
2608 }
2609 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2610 // Shares the INSERT body; the replace flag lowers it
2611 // onto ON CONFLICT DO UPDATE with an empty assignment
2612 // list (engine: replace the whole row).
2613 Token::Ident(s)
2614 if s.eq_ignore_ascii_case("replace")
2615 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2616 {
2617 self.parse_insert_stmt(true)
2618 }
2619 Token::Begin => {
2620 self.advance();
2621 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2622 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2623 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2624 // is consumed first, then the trailing modes — including the
2625 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2626 // WORK/TRANSACTION). The explicit level, when present, rides the
2627 // statement so `exec_begin` applies it for this transaction.
2628 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2629 {
2630 self.advance();
2631 }
2632 let iso = self.parse_isolation_level_clauses()?;
2633 Ok(Statement::Begin(iso))
2634 }
2635 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2636 // for BEGIN. START is contextual in PG too; pattern-match
2637 // on the ident here. Iso clauses are parse-and-ignored,
2638 // same as BEGIN above.
2639 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2640 self.advance();
2641 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2642 {
2643 return Err(self.err(alloc::format!(
2644 "expected TRANSACTION after START, got {:?}",
2645 self.peek()
2646 )));
2647 }
2648 self.advance();
2649 let iso = self.parse_isolation_level_clauses()?;
2650 Ok(Statement::Begin(iso))
2651 }
2652 Token::Commit => {
2653 self.advance();
2654 Ok(Statement::Commit)
2655 }
2656 Token::Rollback => {
2657 self.advance();
2658 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2659 // savepoint without ending the transaction. Bare
2660 // `ROLLBACK` drops the whole TX.
2661 if matches!(self.peek(), Token::To) {
2662 self.advance();
2663 if matches!(self.peek(), Token::Savepoint) {
2664 self.advance();
2665 }
2666 let name = self.expect_ident_like()?;
2667 Ok(Statement::RollbackToSavepoint(name))
2668 } else {
2669 Ok(Statement::Rollback)
2670 }
2671 }
2672 Token::Savepoint => {
2673 self.advance();
2674 let name = self.expect_ident_like()?;
2675 Ok(Statement::Savepoint(name))
2676 }
2677 Token::Release => {
2678 self.advance();
2679 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2680 // is optional in standard SQL.
2681 if matches!(self.peek(), Token::Savepoint) {
2682 self.advance();
2683 }
2684 let name = self.expect_ident_like()?;
2685 Ok(Statement::ReleaseSavepoint(name))
2686 }
2687 Token::Show => {
2688 self.advance();
2689 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2690 // v6.1.2 promoted TABLES to a reserved keyword (for
2691 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2692 // arrives as `Token::Tables` rather than a bare ident.
2693 // USERS / COLUMNS remain bare idents.
2694 let target = match self.advance() {
2695 Token::Tables => "tables".to_string(),
2696 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2697 // keyword token; recognise it as the SHOW CREATE
2698 // dispatch keyword too.
2699 Token::Create => "create".to_string(),
2700 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2701 // keyword too; let SHOW INDEX FROM parse.
2702 Token::Index => "index".to_string(),
2703 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2704 // reserved (used in aggregate function calls);
2705 // recognise it here so the parser dispatches
2706 // to ShowParameter("all") — the engine returns
2707 // the curated parameter inventory.
2708 Token::All => "all".to_string(),
2709 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2710 other => {
2711 return Err(self.err(format!(
2712 "expected SHOW target, got {other:?}"
2713 )));
2714 }
2715 };
2716 match target.as_str() {
2717 "tables" => Ok(Statement::ShowTables),
2718 "users" => Ok(Statement::ShowUsers),
2719 // v7.38 轴 4 — `SHOW transaction_isolation`
2720 // returns the currently-selected isolation level.
2721 "transaction_isolation" => Ok(Statement::ShowParameter(
2722 "transaction_isolation".to_string(),
2723 )),
2724 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2725 // TABLE <t>` returns a 2-column row: (Table,
2726 // Create Table). mysqldump emits this for every
2727 // table at scrape time; without it the dump
2728 // round-trip stalls.
2729 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2730 // FROM <t>` (also spelled `SHOW INDEX` and
2731 // `SHOW KEYS`). admin / mysqldump probes use
2732 // it to list per-table indexes.
2733 "indexes" | "index" | "keys" => {
2734 if !matches!(self.peek(), Token::From) {
2735 return Err(self.err(format!(
2736 "expected FROM after SHOW INDEXES, got {:?}",
2737 self.peek()
2738 )));
2739 }
2740 self.advance();
2741 let table = self.expect_ident_like()?;
2742 Ok(Statement::ShowIndexes(table))
2743 }
2744 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2745 // `SHOW VARIABLES`. Both return a 2-column row
2746 // set listing server-side state; clients probe
2747 // them at connect time.
2748 "status" => Ok(Statement::ShowStatus),
2749 "variables" => Ok(Statement::ShowVariables),
2750 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2751 "processlist" => Ok(Statement::ShowProcesslist),
2752 "create" => {
2753 // SHOW CREATE TABLE / VIEW / DATABASE — only
2754 // TABLE is supported in v7.17.
2755 let kind = match self.advance() {
2756 Token::Ident(s) | Token::QuotedIdent(s) => s,
2757 Token::Table => "table".to_string(),
2758 other => {
2759 return Err(self.err(format!(
2760 "expected TABLE after SHOW CREATE, got {other:?}"
2761 )));
2762 }
2763 };
2764 if !kind.eq_ignore_ascii_case("table") {
2765 return Err(self.err(format!(
2766 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2767 )));
2768 }
2769 let name = self.expect_ident_like()?;
2770 Ok(Statement::ShowCreateTable(name))
2771 }
2772 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2773 // (and `SHOW SCHEMAS` alias). The mysql client uses
2774 // it to populate the database selector at connect
2775 // time; without it `mysql -p` errors before the
2776 // first user query.
2777 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2778 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2779 // keyword on its own; it lands here as a bare
2780 // ident. Returning all publications + their
2781 // scope summary.
2782 "publications" => Ok(Statement::ShowPublications),
2783 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2784 "subscriptions" => Ok(Statement::ShowSubscriptions),
2785 "columns" => {
2786 if !matches!(self.peek(), Token::From) {
2787 return Err(self.err(format!(
2788 "expected FROM after SHOW COLUMNS, got {:?}",
2789 self.peek()
2790 )));
2791 }
2792 self.advance();
2793 let table = self.expect_ident_like()?;
2794 Ok(Statement::ShowColumns(table))
2795 }
2796 // v7.38 轴 4 surface — `SHOW <param>` for any
2797 // remaining session / preset parameter name
2798 // (server_version, search_path, client_encoding,
2799 // …). The engine's ShowParameter handler does the
2800 // dispatch; unrecognised names error there with
2801 // a pointer to pg_settings, not at parse time —
2802 // so a driver that issues `SHOW spam_setting`
2803 // gets a clear runtime error instead of a
2804 // confusing "unknown SHOW target".
2805 other => {
2806 // v7.38 (read01 P3.20) — a custom namespaced GUC
2807 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2808 // consume the dotted tail so it round-trips with
2809 // `SET app.foo` / `current_setting('app.foo')`.
2810 let mut full = other.to_string();
2811 while matches!(self.peek(), Token::Dot) {
2812 self.advance();
2813 let seg = self.expect_ident_like()?;
2814 full.push('.');
2815 full.push_str(&seg.to_ascii_lowercase());
2816 }
2817 Ok(Statement::ShowParameter(full))
2818 }
2819 }
2820 }
2821 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2822 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2823 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2824 // arrived as a bare ident; tokenising it dedicatedly
2825 // keeps the dispatch tree small.
2826 Token::Drop => {
2827 self.advance();
2828 match self.peek() {
2829 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2830 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2831 // around DROP ROLE cleanup. SPG has no role-owner
2832 // model, so consume to boundary as a no-op.
2833 Token::Ident(s) | Token::QuotedIdent(s)
2834 if s.eq_ignore_ascii_case("owned") =>
2835 {
2836 // v7.39 (round 696) — still a no-op (SPG has no
2837 // role-owner model), but the ROLE is carried out so
2838 // the engine can refuse one that does not exist,
2839 // which is what PG18 does.
2840 self.advance();
2841 if self.peek_is_by() {
2842 self.advance();
2843 }
2844 let names = self.take_comma_separated_names();
2845 self.consume_until_statement_boundary();
2846 Ok(Statement::ValidateOnly {
2847 kind: crate::ast::ValidateOnlyKind::RoleName,
2848 names,
2849 })
2850 }
2851 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2852 // It drops only a TEMPORARY table, and name resolution
2853 // already prefers the session's own, so the keyword is
2854 // consumed and the ordinary DROP TABLE path runs.
2855 Token::Ident(s) | Token::QuotedIdent(s)
2856 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2857 {
2858 self.advance();
2859 if !matches!(self.peek(), Token::Table) {
2860 return Err(self.err(alloc::format!(
2861 "expected TABLE after DROP TEMPORARY, got {:?}",
2862 self.peek()
2863 )));
2864 }
2865 self.parse_drop_table_after_keyword()
2866 }
2867 Token::Publication => {
2868 self.advance();
2869 // v7.39 (round 754, F31-B4) — the round-753
2870 // audit probe tripped over the missing
2871 // `IF EXISTS` here (syntax error).
2872 let if_exists = self.consume_if_exists();
2873 let name = self.expect_ident_or_string()?;
2874 Ok(Statement::DropPublication { name, if_exists })
2875 }
2876 Token::Subscription => {
2877 self.advance();
2878 let if_exists = self.consume_if_exists();
2879 let name = self.expect_ident_or_string()?;
2880 Ok(Statement::DropSubscription { name, if_exists })
2881 }
2882 Token::Ident(s) | Token::QuotedIdent(s)
2883 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2884 {
2885 self.advance();
2886 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2887 // login user IS a role in PG, and SPG's store holds
2888 // both. `IF EXISTS` is accepted on either spelling.
2889 let if_exists = self.consume_if_exists();
2890 let name = self.expect_ident_or_string()?;
2891 Ok(Statement::DropUser { name, if_exists })
2892 }
2893 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2894 // CREATE DATABASE has parsed since v7.14 and this did
2895 // not, so `DROP DATABASE IF EXISTS x` — what every
2896 // teardown script and pg_dumpall preamble opens with —
2897 // came back as a syntax error, which IF EXISTS cannot
2898 // soften. The name is carried so the engine can answer
2899 // the way PG does; PG never lets this succeed on a
2900 // single-database server, since the name is either
2901 // unknown ("database … does not exist", or a notice
2902 // under IF EXISTS) or the one you are connected to
2903 // ("cannot drop the currently open database").
2904 Token::Ident(s) | Token::QuotedIdent(s)
2905 if s.eq_ignore_ascii_case("database") =>
2906 {
2907 self.advance();
2908 let if_exists = self.consume_if_exists();
2909 let name = self.expect_ident_or_string()?;
2910 self.consume_until_statement_boundary();
2911 Ok(Statement::DropDatabase { name, if_exists })
2912 }
2913 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2914 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2915 self.advance();
2916 let if_exists = self.consume_if_exists();
2917 let name = self.expect_ident_like()?;
2918 // ON <table>
2919 if !matches!(self.peek(), Token::On) {
2920 return Err(self.err(alloc::format!(
2921 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2922 self.peek()
2923 )));
2924 }
2925 self.advance();
2926 let table = self.expect_ident_like()?;
2927 Ok(Statement::DropTrigger {
2928 name,
2929 table,
2930 if_exists,
2931 })
2932 }
2933 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2934 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2935 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2936 self.advance();
2937 let if_exists = self.consume_if_exists();
2938 let name = self.expect_ident_like()?;
2939 if !matches!(self.peek(), Token::On) {
2940 return Err(self.err(alloc::format!(
2941 "expected ON <table> after DROP RULE {name:?}, got {:?}",
2942 self.peek()
2943 )));
2944 }
2945 self.advance();
2946 let table = self.expect_ident_like()?;
2947 // Optional CASCADE / RESTRICT — accepted, no effect.
2948 self.consume_until_statement_boundary();
2949 Ok(Statement::DropRule {
2950 name,
2951 table,
2952 if_exists,
2953 })
2954 }
2955 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2956 // v7.12.4 ignores any optional arg-list (signature-
2957 // based overload disambiguation lands in v7.12.5+).
2958 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2959 self.advance();
2960 let if_exists = self.consume_if_exists();
2961 let name = self.expect_ident_like()?;
2962 // v7.39 (read01 round 62) — the argument list identifies
2963 // WHICH overload to drop, so it is captured, not
2964 // discarded. `DROP FUNCTION f` (no list) is legal when
2965 // the name is unambiguous; the engine enforces that.
2966 let args = if matches!(self.peek(), Token::LParen) {
2967 Some(self.parse_function_signature_types()?)
2968 } else {
2969 None
2970 };
2971 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2972 // trailer, which `DROP TABLE` and `DROP INDEX` have
2973 // accepted since v7.14 and this one refused outright.
2974 // pg_dump writes it, so refusing was a parse error in
2975 // the middle of a restore. SPG drops the function
2976 // either way — it tracks no dependents to cascade to —
2977 // which is the same reading the other two give it.
2978 self.consume_drop_behaviour();
2979 Ok(Statement::DropFunction {
2980 name,
2981 args,
2982 if_exists,
2983 })
2984 }
2985 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2986 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2987 // emit DROP TABLE IF EXISTS at the head of every
2988 // CREATE TABLE block so re-importing a dump
2989 // overwrites prior state. SPG accepts and removes
2990 // matching tables; CASCADE/RESTRICT trailers
2991 // accepted silently.
2992 Token::Table => self.parse_drop_table_after_keyword(),
2993 // v7.14.0 — DROP INDEX [IF EXISTS] name
2994 // [CASCADE|RESTRICT]. PG / mysqldump emit this
2995 // for partial-index renames and pgvector
2996 // migrations. SPG removes the matching index;
2997 // IF EXISTS makes the drop idempotent.
2998 Token::Index => {
2999 self.advance();
3000 let if_exists = self.consume_if_exists();
3001 let name = self.expect_ident_like()?;
3002 if matches!(
3003 self.peek(),
3004 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3005 || s.eq_ignore_ascii_case("restrict")
3006 ) {
3007 self.advance();
3008 }
3009 Ok(Statement::DropIndex { name, if_exists })
3010 }
3011 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3012 // [CASCADE|RESTRICT]. SPG is single-database;
3013 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3014 // name [, name…] [CASCADE | RESTRICT]. Real
3015 // unregister (was silent no-op pre-v7.17).
3016 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3017 self.advance();
3018 let if_exists = self.consume_if_exists();
3019 let mut names = vec![self.expect_ident_like()?];
3020 while matches!(self.peek(), Token::Comma) {
3021 self.advance();
3022 names.push(self.expect_ident_like()?);
3023 }
3024 if matches!(
3025 self.peek(),
3026 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3027 || s.eq_ignore_ascii_case("restrict")
3028 ) {
3029 self.advance();
3030 }
3031 Ok(Statement::DropSchema { names, if_exists })
3032 }
3033 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3034 // name [, name…] [CASCADE|RESTRICT].
3035 Token::Ident(s) | Token::QuotedIdent(s)
3036 if s.eq_ignore_ascii_case("type") =>
3037 {
3038 self.advance();
3039 let if_exists = self.consume_if_exists();
3040 let mut names = vec![self.expect_ident_like()?];
3041 while matches!(self.peek(), Token::Comma) {
3042 self.advance();
3043 names.push(self.expect_ident_like()?);
3044 }
3045 if matches!(
3046 self.peek(),
3047 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3048 || s.eq_ignore_ascii_case("restrict")
3049 ) {
3050 self.advance();
3051 }
3052 Ok(Statement::DropType { names, if_exists })
3053 }
3054 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3055 // name [, name…] [CASCADE|RESTRICT].
3056 Token::Ident(s) | Token::QuotedIdent(s)
3057 if s.eq_ignore_ascii_case("domain") =>
3058 {
3059 self.advance();
3060 let if_exists = self.consume_if_exists();
3061 let mut names = vec![self.expect_ident_like()?];
3062 while matches!(self.peek(), Token::Comma) {
3063 self.advance();
3064 names.push(self.expect_ident_like()?);
3065 }
3066 if matches!(
3067 self.peek(),
3068 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3069 || s.eq_ignore_ascii_case("restrict")
3070 ) {
3071 self.advance();
3072 }
3073 Ok(Statement::DropDomain { names, if_exists })
3074 }
3075 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3076 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3077 Token::Ident(s) | Token::QuotedIdent(s)
3078 if s.eq_ignore_ascii_case("materialized") =>
3079 {
3080 self.advance();
3081 let nxt = self.peek().clone();
3082 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3083 {
3084 return Err(self.err(alloc::format!(
3085 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3086 )));
3087 }
3088 self.advance();
3089 let if_exists = self.consume_if_exists();
3090 let mut names = vec![self.expect_ident_like()?];
3091 while matches!(self.peek(), Token::Comma) {
3092 self.advance();
3093 names.push(self.expect_ident_like()?);
3094 }
3095 if matches!(
3096 self.peek(),
3097 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3098 || s.eq_ignore_ascii_case("restrict")
3099 ) {
3100 self.advance();
3101 }
3102 Ok(Statement::DropMaterializedView { names, if_exists })
3103 }
3104 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3105 // name [, name…] [CASCADE|RESTRICT].
3106 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3107 self.advance();
3108 let if_exists = self.consume_if_exists();
3109 let mut names = vec![self.expect_ident_like()?];
3110 while matches!(self.peek(), Token::Comma) {
3111 self.advance();
3112 names.push(self.expect_ident_like()?);
3113 }
3114 if matches!(
3115 self.peek(),
3116 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3117 || s.eq_ignore_ascii_case("restrict")
3118 ) {
3119 self.advance();
3120 }
3121 Ok(Statement::DropView { names, if_exists })
3122 }
3123 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3124 // [CASCADE|RESTRICT]. Real removal from catalog
3125 // (was a silent no-op pre-v7.17).
3126 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3127 self.advance();
3128 let if_exists = self.consume_if_exists();
3129 let mut names = vec![self.expect_ident_like()?];
3130 while matches!(self.peek(), Token::Comma) {
3131 self.advance();
3132 names.push(self.expect_ident_like()?);
3133 }
3134 if matches!(
3135 self.peek(),
3136 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3137 || s.eq_ignore_ascii_case("restrict")
3138 ) {
3139 self.advance();
3140 }
3141 Ok(Statement::DropSequence { names, if_exists })
3142 }
3143 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3144 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3145 self.advance();
3146 self.parse_drop_policy_after_keyword()
3147 }
3148 // v7.37.17 (17.6 siblings) — DROP <target> for
3149 // targets SPG doesn't natively track. pg_dump
3150 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3151 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3152 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3153 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3154 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3155 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3156 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3157 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3158 // etc. — accept + Empty-return so pg_dump tails
3159 // load through. Materialized-view drop dispatches
3160 // to the existing DropTable path when the token
3161 // is Materialized-View-shaped (elsewhere in
3162 // this parser).
3163 Token::Ident(s) | Token::QuotedIdent(s)
3164 if s.eq_ignore_ascii_case("text")
3165 // The DROP dispatch matches on PEEK — `text` is
3166 // not yet consumed, so SEARCH/CONFIGURATION sit
3167 // at pos+1/pos+2 (the round-695 trap's mirror).
3168 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3169 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3170 {
3171 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3172 // validates the name; DICTIONARY / PARSER / TEMPLATE
3173 // stay in the noise arm below.
3174 self.advance(); // TEXT
3175 self.advance(); // SEARCH
3176 self.advance(); // CONFIGURATION
3177 let if_exists = self.consume_if_exists();
3178 let names = self.take_comma_separated_names();
3179 self.consume_until_statement_boundary();
3180 if if_exists {
3181 return Ok(Statement::Empty);
3182 }
3183 Ok(Statement::ValidateOnly {
3184 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3185 names,
3186 })
3187 }
3188 Token::Ident(s) | Token::QuotedIdent(s)
3189 if matches!(
3190 s.to_ascii_lowercase().as_str(),
3191 "type"
3192 | "domain"
3193 | "operator"
3194 | "cast"
3195 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3196 // TEMPLATE (CONFIGURATION intercepted above).
3197 | "text"
3198 | "materialized"
3199 | "large"
3200 | "role"
3201 | "access"
3202 | "procedure"
3203 | "routine"
3204 ) =>
3205 {
3206 self.consume_until_statement_boundary();
3207 Ok(Statement::Empty)
3208 }
3209 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3210 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3211 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3212 // foreign-data warning family (round 706) so a
3213 // CREATE→DROP sequence in a dump stays consistent.
3214 Token::Ident(s) | Token::QuotedIdent(s)
3215 if s.eq_ignore_ascii_case("server")
3216 || s.eq_ignore_ascii_case("foreign") =>
3217 {
3218 self.advance();
3219 self.consume_until_statement_boundary();
3220 Ok(Statement::ValidateOnly {
3221 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3222 names: Vec::new(),
3223 })
3224 }
3225 Token::Ident(s) | Token::QuotedIdent(s)
3226 if s.eq_ignore_ascii_case("collation")
3227 || s.eq_ignore_ascii_case("tablespace") =>
3228 {
3229 let kind = if s.eq_ignore_ascii_case("collation") {
3230 crate::ast::ValidateOnlyKind::CollationName
3231 } else {
3232 crate::ast::ValidateOnlyKind::TablespaceName
3233 };
3234 self.advance();
3235 let if_exists = self.consume_if_exists();
3236 let names = self.take_comma_separated_names();
3237 self.consume_until_statement_boundary();
3238 if if_exists {
3239 return Ok(Statement::Empty);
3240 }
3241 Ok(Statement::ValidateOnly { kind, names })
3242 }
3243 Token::Ident(s) | Token::QuotedIdent(s)
3244 if s.eq_ignore_ascii_case("event") =>
3245 {
3246 self.advance();
3247 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3248 {
3249 self.advance();
3250 }
3251 let if_exists = self.consume_if_exists();
3252 let names = self.take_comma_separated_names();
3253 self.consume_until_statement_boundary();
3254 if if_exists {
3255 return Ok(Statement::Empty);
3256 }
3257 Ok(Statement::ValidateOnly {
3258 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3259 names,
3260 })
3261 }
3262 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3263 // leave the noise list; see the ValidateOnly kinds.
3264 Token::Ident(s) | Token::QuotedIdent(s)
3265 if s.eq_ignore_ascii_case("conversion")
3266 || s.eq_ignore_ascii_case("language")
3267 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3268 // FIRST — the first draft looked for it after.
3269 || s.eq_ignore_ascii_case("procedural") =>
3270 {
3271 let kind = if s.eq_ignore_ascii_case("conversion") {
3272 crate::ast::ValidateOnlyKind::ConversionName
3273 } else {
3274 crate::ast::ValidateOnlyKind::LanguageName
3275 };
3276 self.advance();
3277 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3278 {
3279 self.advance();
3280 }
3281 let if_exists = self.consume_if_exists();
3282 let names = self.take_comma_separated_names();
3283 self.consume_until_statement_boundary();
3284 if if_exists {
3285 return Ok(Statement::Empty);
3286 }
3287 Ok(Statement::ValidateOnly { kind, names })
3288 }
3289 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3290 // name(argtypes)[, …]`. Parsed for real so the engine
3291 // can answer as PG does; see Statement::DropAggregate.
3292 Token::Ident(s) | Token::QuotedIdent(s)
3293 if s.eq_ignore_ascii_case("aggregate") =>
3294 {
3295 self.advance();
3296 let if_exists = self.consume_if_exists();
3297 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3298 loop {
3299 let name = self.expect_ident_like()?;
3300 if !matches!(self.peek(), Token::LParen) {
3301 return Err(self.err(alloc::format!(
3302 "expected argument list after DROP AGGREGATE {name}"
3303 )));
3304 }
3305 self.advance();
3306 let mut args: Vec<String> = Vec::new();
3307 let mut star = false;
3308 loop {
3309 match self.peek().clone() {
3310 Token::RParen => {
3311 self.advance();
3312 break;
3313 }
3314 Token::Star => {
3315 self.advance();
3316 star = true;
3317 }
3318 Token::Comma => {
3319 self.advance();
3320 }
3321 _ => {
3322 // A type name may be multi-token
3323 // (`double precision`); glue idents
3324 // until , or ).
3325 let mut t = self.expect_ident_like()?;
3326 while let Token::Ident(nx) = self.peek() {
3327 let nx = nx.clone();
3328 self.advance();
3329 t.push(' ');
3330 t.push_str(&nx);
3331 }
3332 args.push(t);
3333 }
3334 }
3335 }
3336 items.push((name, if star { None } else { Some(args) }));
3337 if matches!(self.peek(), Token::Comma) {
3338 self.advance();
3339 } else {
3340 break;
3341 }
3342 }
3343 self.consume_until_statement_boundary();
3344 Ok(Statement::DropAggregate { if_exists, items })
3345 }
3346 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3347 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3348 // installed; `IF EXISTS` is the spelling that says do
3349 // not, and it keeps the no-op.
3350 Token::Ident(s) | Token::QuotedIdent(s)
3351 if s.eq_ignore_ascii_case("extension") =>
3352 {
3353 self.advance();
3354 let if_exists = self.consume_if_exists();
3355 let names = self.take_comma_separated_names();
3356 self.consume_until_statement_boundary();
3357 if if_exists {
3358 return Ok(Statement::Empty);
3359 }
3360 Ok(Statement::ValidateOnly {
3361 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3362 names,
3363 })
3364 }
3365 Token::Ident(s) | Token::QuotedIdent(s)
3366 if s.eq_ignore_ascii_case("statistics") =>
3367 {
3368 self.parse_drop_statistics_after_drop()
3369 }
3370 other => Err(self.err(format!(
3371 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3372 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3373 ))),
3374 }
3375 }
3376 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3377 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3378 // and accepted before the view name. SPG materialised
3379 // views re-evaluate on read (always-fresh semantics), so
3380 // the CONCURRENTLY-vs-serial distinction has no runtime
3381 // effect — the refresh body does not block readers either
3382 // way. Same accept-and-no-op pattern as DETACH PARTITION
3383 // CONCURRENTLY (16.5).
3384 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3385 self.advance();
3386 let nxt = self.peek().clone();
3387 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3388 {
3389 return Err(self.err(alloc::format!(
3390 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3391 )));
3392 }
3393 self.advance();
3394 let nxt2 = self.peek().clone();
3395 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3396 {
3397 return Err(self.err(alloc::format!(
3398 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3399 )));
3400 }
3401 self.advance();
3402 // Optional CONCURRENTLY noise word — consumed without
3403 // changing semantics.
3404 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3405 {
3406 self.advance();
3407 }
3408 let name = self.expect_ident_like()?;
3409 let with_data = self.parse_optional_with_data(true)?;
3410 Ok(Statement::RefreshMaterializedView { name, with_data })
3411 }
3412 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3413 self.advance();
3414 self.parse_update_after_keyword()
3415 }
3416 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3417 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3418 // [CASCADE | RESTRICT]. Clears every row from each named
3419 // table. Parses at the top level; the engine dispatcher
3420 // walks Statement::Truncate.
3421 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3422 self.advance();
3423 // Optional TABLE noise word — PG accepts both the reserved
3424 // token and the bare identifier spelling.
3425 if matches!(self.peek(), Token::Table)
3426 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3427 {
3428 self.advance();
3429 }
3430 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3431 // not absorbed. The lookahead keeps a table genuinely
3432 // called `only` working: the keyword is a keyword only
3433 // when a name follows it.
3434 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3435 if s.eq_ignore_ascii_case("only"))
3436 && matches!(
3437 self.tokens.get(self.pos + 1),
3438 Some(Token::Ident(_) | Token::QuotedIdent(_))
3439 );
3440 if only {
3441 self.advance();
3442 }
3443 // Table names (comma-separated).
3444 let mut tables = Vec::new();
3445 loop {
3446 tables.push(self.expect_ident_like()?);
3447 if matches!(self.peek(), Token::Comma) {
3448 self.advance();
3449 continue;
3450 }
3451 break;
3452 }
3453 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3454 let mut restart_identity = false;
3455 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3456 {
3457 self.advance();
3458 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3459 {
3460 self.advance();
3461 restart_identity = true;
3462 }
3463 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3464 {
3465 self.advance();
3466 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3467 {
3468 self.advance();
3469 }
3470 }
3471 // Optional CASCADE / RESTRICT.
3472 let mut cascade = false;
3473 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3474 {
3475 self.advance();
3476 cascade = true;
3477 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3478 {
3479 self.advance();
3480 }
3481 Ok(Statement::Truncate {
3482 tables,
3483 restart_identity,
3484 cascade,
3485 only,
3486 })
3487 }
3488 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3489 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3490 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3491 // rows change so the index tree is always up-to-date;
3492 // REINDEX is a strict no-op. Accept the whole statement
3493 // shape to boundary for pg_dump round-trip compatibility.
3494 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3495 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3496 // index bloat to rebuild, so the work stays a no-op, but PG
3497 // validates what it was pointed at and this swallowed the
3498 // name at parse time — `REINDEX TABLE typo` reported
3499 // success. Measured on PG18: INDEX / TABLE name a relation,
3500 // SCHEMA a schema, SYSTEM nothing.
3501 self.advance();
3502 self.parse_reindex_tail()
3503 }
3504 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3505 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3506 // SPG has no MVCC bloat today (Phase D visibility map
3507 // queues with v7.38); the freezer collapses hot-tier
3508 // rows into cold segments automatically. VACUUM is a
3509 // no-op — pg_dump maintenance scripts and Discourse's
3510 // periodic-maintenance path both emit it.
3511 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3512 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3513 // actual bloat, so the pre-MVCC accept-and-ignore posture
3514 // became a silent no-op on a customer's manual reclaim.
3515 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3516 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3517 // ANALYZE is captured, the optional table name is captured.
3518 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3519 self.advance();
3520 // Parenthesised option list: absorb it.
3521 if matches!(self.peek(), Token::LParen) {
3522 let mut depth = 0usize;
3523 loop {
3524 match self.advance() {
3525 Token::LParen => depth += 1,
3526 Token::RParen => {
3527 depth -= 1;
3528 if depth == 0 {
3529 break;
3530 }
3531 }
3532 Token::Eof => break,
3533 _ => {}
3534 }
3535 }
3536 }
3537 let mut analyze = false;
3538 let mut table: Option<String> = None;
3539 loop {
3540 match self.peek() {
3541 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3542 // an identifier, so the loop below broke out on it and
3543 // dropped the table name: `VACUUM FULL nosuch` was
3544 // accepted where `VACUUM nosuch` was refused.
3545 Token::Full => {
3546 self.advance();
3547 }
3548 Token::Ident(w) | Token::QuotedIdent(w) => {
3549 let wl = w.to_ascii_lowercase();
3550 match wl.as_str() {
3551 "full" | "freeze" | "verbose" => {
3552 self.advance();
3553 }
3554 "analyze" | "analyse" => {
3555 analyze = true;
3556 self.advance();
3557 }
3558 _ => {
3559 table = Some(self.expect_ident_like()?);
3560 break;
3561 }
3562 }
3563 }
3564 _ => break,
3565 }
3566 }
3567 // Optional trailing column list / anything else to the
3568 // statement boundary (PG accepts per-column ANALYZE).
3569 self.consume_until_statement_boundary();
3570 Ok(Statement::Vacuum { table, analyze })
3571 }
3572 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3573 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3574 // <index>. PG stores rows in physical order matching
3575 // an index; SPG's hot-tier is append-only + cold-tier
3576 // is segment-frozen, so clustering has no persistent
3577 // effect. Accept-and-no-op for pg_dump compat.
3578 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3579 // v7.39 (round 535) — same as REINDEX above: the relation is
3580 // carried so the engine can refuse one that does not exist.
3581 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3582 self.advance();
3583 self.parse_cluster_tail()
3584 }
3585 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3586 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3587 // optional string payload; UNLISTEN takes a channel or `*`.
3588 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3589 self.advance();
3590 let ch = match self.advance() {
3591 Token::Ident(c) | Token::QuotedIdent(c) => c,
3592 other => {
3593 return Err(self.err(format!(
3594 "expected channel name after LISTEN, got {other:?}"
3595 )));
3596 }
3597 };
3598 Ok(Statement::Listen(ch))
3599 }
3600 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3601 self.advance();
3602 let channel = match self.advance() {
3603 Token::Ident(c) | Token::QuotedIdent(c) => c,
3604 other => {
3605 return Err(self.err(format!(
3606 "expected channel name after NOTIFY, got {other:?}"
3607 )));
3608 }
3609 };
3610 let payload = if matches!(self.peek(), Token::Comma) {
3611 self.advance();
3612 match self.advance() {
3613 Token::String(p) => Some(p),
3614 other => {
3615 return Err(self.err(format!(
3616 "expected string payload after NOTIFY <channel>, got {other:?}"
3617 )));
3618 }
3619 }
3620 } else {
3621 None
3622 };
3623 Ok(Statement::Notify { channel, payload })
3624 }
3625 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3626 self.advance();
3627 match self.advance() {
3628 Token::Star => Ok(Statement::Unlisten(None)),
3629 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3630 other => Err(self.err(format!(
3631 "expected channel name or * after UNLISTEN, got {other:?}"
3632 ))),
3633 }
3634 }
3635 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3636 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3637 // process-wide write lock today; explicit LOCK has no
3638 // effect. Accept-and-no-op for pg_dump / migration
3639 // compat.
3640 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3641 self.advance();
3642 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3643 // engine holds a process-wide write lock), but the TABLE
3644 // NAME is now carried out so the engine can refuse one that
3645 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3646 // READ|WRITE` is a different statement with the same first
3647 // word; it keeps the old no-op, because a MySQL dump's
3648 // bracket names tables it is about to create.
3649 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3650 if k.eq_ignore_ascii_case("tables"));
3651 if mysql_tables {
3652 self.consume_until_statement_boundary();
3653 return Ok(Statement::Empty);
3654 }
3655 if matches!(self.peek(), Token::Table) {
3656 self.advance();
3657 }
3658 let names = self.take_comma_separated_names();
3659 self.consume_until_statement_boundary();
3660 Ok(Statement::ValidateOnly {
3661 kind: crate::ast::ValidateOnlyKind::LockTable,
3662 names,
3663 })
3664 }
3665 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3666 // durability marker + snapshot in PG. SPG has WAL
3667 // checkpointing on a byte / time schedule (v7.37.10
3668 // 60s / 4 MiB defaults). The bare statement parses to
3669 // `Statement::Empty` here (the no_std engine owns no
3670 // WAL / snapshot); v7.37 Epic Du wires the HOST
3671 // (embedded `Database::execute_buffered`, via
3672 // `sql_is_checkpoint`) to force an immediate synchronous
3673 // checkpoint through `Database::checkpoint` — a real
3674 // durability barrier, matching PG.
3675 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3676 self.advance();
3677 self.consume_until_statement_boundary();
3678 Ok(Statement::Empty)
3679 }
3680 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3681 self.advance();
3682 self.parse_delete_after_keyword()
3683 }
3684 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3685 // ALTER is not a reserved keyword in the lexer — handled
3686 // as a bare ident here.
3687 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3688 self.advance();
3689 self.parse_alter_after_keyword()
3690 }
3691 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3692 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3693 // additions needed.
3694 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3695 self.advance();
3696 self.parse_wait_after_keyword()
3697 }
3698 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3699 // Bare ANALYZE → analyse every user table; ANALYZE
3700 // <name> → re-stats one. The argument is an optional
3701 // ident (or quoted ident); anything else is a parse
3702 // error.
3703 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3704 // `WHERE` filter (carved out per V6_7_DESIGN.md
3705 // STABILITY). Lex order: identifier "compact" → "cold"
3706 // → "segments". Anything else after `COMPACT` is a
3707 // parse error.
3708 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3709 self.advance();
3710 let next = self.peek().clone();
3711 let cold = match next {
3712 Token::Ident(s) | Token::QuotedIdent(s) => s,
3713 _ => {
3714 return Err(
3715 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3716 );
3717 }
3718 };
3719 if !cold.eq_ignore_ascii_case("cold") {
3720 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3721 }
3722 self.advance();
3723 let next = self.peek().clone();
3724 let segments = match next {
3725 Token::Ident(s) | Token::QuotedIdent(s) => s,
3726 _ => {
3727 return Err(self.err(format!(
3728 "expected SEGMENTS after COMPACT COLD, got {:?}",
3729 self.peek()
3730 )));
3731 }
3732 };
3733 if !segments.eq_ignore_ascii_case("segments") {
3734 return Err(self.err(format!(
3735 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3736 )));
3737 }
3738 self.advance();
3739 Ok(Statement::CompactColdSegments)
3740 }
3741 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3742 // Parsed as a case-insensitive identifier since MERGE
3743 // isn't a reserved lexer keyword (collides with the
3744 // mysqldump `ALGORITHM = MERGE` view clause if it
3745 // were); the inner parser drives the rest of the
3746 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3747 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3748 self.advance();
3749 self.parse_merge_after_keyword()
3750 }
3751 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3752 self.advance();
3753 let target = match self.peek() {
3754 Token::Eof | Token::Semicolon => None,
3755 Token::Ident(_) | Token::QuotedIdent(_) => {
3756 Some(self.expect_ident_like()?)
3757 }
3758 other => {
3759 return Err(self.err(format!(
3760 "expected table name or end of statement after ANALYZE, got {other:?}"
3761 )));
3762 }
3763 };
3764 // v7.39 (round 776, F31 J7) — the per-column form
3765 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3766 // here while the VACUUM arm already consumed it; SPG
3767 // analyzes whole tables, so the list parses and is
3768 // accepted like the VACUUM path's.
3769 if target.is_some() && matches!(self.peek(), Token::LParen) {
3770 self.advance();
3771 loop {
3772 let _ = self.expect_ident_like()?;
3773 match self.peek() {
3774 Token::Comma => {
3775 self.advance();
3776 }
3777 Token::RParen => {
3778 self.advance();
3779 break;
3780 }
3781 other => {
3782 return Err(self.err(format!(
3783 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3784 )));
3785 }
3786 }
3787 }
3788 }
3789 Ok(Statement::Analyze(target))
3790 }
3791 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3792 // `default_text_search_config` parameter is consumed
3793 // by the FTS function dispatcher; other parameter
3794 // names are recorded but treated as a no-op so PG
3795 // dump output loads.
3796 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3797 self.advance();
3798 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3799 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3800 // …` which the SessionVar path handles). `LOCAL` is the only
3801 // one that changes semantics — it scopes the change to the
3802 // current transaction — so capture it; SESSION / GLOBAL are
3803 // accepted and treated as the default session scope.
3804 let mut set_local = false;
3805 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3806 let q = s.to_ascii_lowercase();
3807 if q == "local" || q == "session" || q == "global" {
3808 set_local = q == "local";
3809 self.advance();
3810 }
3811 }
3812 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3813 // <collation>]` — change the connection client
3814 // charset. SPG stores UTF-8 always and orders
3815 // bytewise; accept as a no-op.
3816 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3817 {
3818 self.advance();
3819 // Charset ident-or-string.
3820 if matches!(
3821 self.peek(),
3822 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3823 ) {
3824 self.advance();
3825 }
3826 // Optional `COLLATE <name>`.
3827 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3828 {
3829 self.advance();
3830 if matches!(
3831 self.peek(),
3832 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3833 ) {
3834 self.advance();
3835 }
3836 }
3837 return Ok(Statement::Empty);
3838 }
3839 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3840 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3841 // uses this to switch to the object owner before
3842 // recreating tables. SPG has no role system so this
3843 // is a no-op.
3844 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3845 {
3846 self.advance(); // ROLE
3847 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3848 // reset to the login identity; a name / string sets the
3849 // effective role that drives current_user + RLS.
3850 let role = match self.peek().clone() {
3851 Token::Default => {
3852 self.advance();
3853 None
3854 }
3855 Token::Ident(s) | Token::QuotedIdent(s)
3856 if s.eq_ignore_ascii_case("none") =>
3857 {
3858 self.advance();
3859 None
3860 }
3861 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3862 self.advance();
3863 Some(s)
3864 }
3865 _ => None,
3866 };
3867 return Ok(Statement::SetRole(role));
3868 }
3869 // v7.37.17 (17.6 sibling) — PG `SET SESSION
3870 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3871 // ISO SQL surface). pg_dump prepends this to fix
3872 // the isolation level for the restore session. SPG
3873 // defaults to READ COMMITTED and doesn't yet honor
3874 // session-set isolation across statements — accept
3875 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3876 // per-tx form is handled elsewhere.
3877 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3878 {
3879 self.advance(); // CHARACTERISTICS
3880 self.consume_until_statement_boundary();
3881 return Ok(Statement::Empty);
3882 }
3883 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3884 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3885 // pg_dump emits this to control the deferrability of
3886 // FK / UNIQUE constraints across a bulk restore. SPG
3887 // has no deferrable-constraint machinery today; the
3888 // FK checker is strict-immediate. Accept-and-no-op
3889 // for pg_dump round-trip compatibility.
3890 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3891 {
3892 self.advance(); // CONSTRAINTS
3893 // v7.39 (round 288) — no longer a no-op: the trailing
3894 // DEFERRED / IMMEDIATE sets the transaction's timing.
3895 // v7.39 (round 308, V29) — and the names are kept.
3896 // They used to be skipped over on the way to the
3897 // DEFERRED keyword, so a named form silently behaved
3898 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3899 // every deferrable constraint in the transaction.
3900 let mut names: alloc::vec::Vec<alloc::string::String> =
3901 alloc::vec::Vec::new();
3902 if matches!(self.peek(), Token::All) {
3903 self.advance();
3904 } else {
3905 loop {
3906 let mut n = self.expect_ident_like()?;
3907 // A schema-qualified name (`public.fk_a`)
3908 // identifies the same constraint; PG resolves
3909 // it by the trailing segment.
3910 while matches!(self.peek(), Token::Dot) {
3911 self.advance();
3912 n = self.expect_ident_like()?;
3913 }
3914 names.push(n);
3915 if matches!(self.peek(), Token::Comma) {
3916 self.advance();
3917 } else {
3918 break;
3919 }
3920 }
3921 }
3922 let deferred = match self.peek() {
3923 Token::Ident(s) | Token::QuotedIdent(s)
3924 if s.eq_ignore_ascii_case("deferred") =>
3925 {
3926 true
3927 }
3928 Token::Ident(s) | Token::QuotedIdent(s)
3929 if s.eq_ignore_ascii_case("immediate") =>
3930 {
3931 false
3932 }
3933 other => {
3934 return Err(self.err(alloc::format!(
3935 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3936 )));
3937 }
3938 };
3939 self.advance();
3940 return Ok(Statement::SetConstraints { names, deferred });
3941 }
3942 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3943 // { DEFAULT | '<role>' | <ident> }` (mailrs
3944 // round-10 A.1). pg_dump preamble emits the
3945 // `DEFAULT` form to reset session authorization.
3946 //
3947 // v7.39 (round 697) — this said "SPG has no role system so
3948 // this is a strict no-op". SPG has had one since round 58;
3949 // the comment outlived it, and with it the reason a name
3950 // that is not a role was accepted here. It still switches
3951 // no authorization — what it does now is refuse a role
3952 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3953 // AUTHORIZATION` (handled by the RESET parser
3954 // elsewhere). Reference:
3955 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3956 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3957 {
3958 self.advance(); // AUTHORIZATION
3959 match self.peek().clone() {
3960 Token::Default => {
3961 self.advance();
3962 }
3963 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3964 self.advance();
3965 return Ok(Statement::ValidateOnly {
3966 kind: crate::ast::ValidateOnlyKind::RoleName,
3967 names: alloc::vec![r],
3968 });
3969 }
3970 other => {
3971 return Err(self.err(alloc::format!(
3972 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3973 )));
3974 }
3975 }
3976 return Ok(Statement::Empty);
3977 }
3978 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3979 // ISOLATION LEVEL { READ COMMITTED | READ
3980 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3981 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3982 // PG-standard surface. v7.37.8 accepts the syntax
3983 // and tracks the selected level on
3984 // `Engine::current_isolation_level()`; the actual
3985 // MVCC / SSI semantics implementation lands in
3986 // the 轴 4 isolation framework (separate train).
3987 // PG itself maps READ UNCOMMITTED to READ COMMITTED
3988 // internally; SPG behaves the same (effectively
3989 // READ COMMITTED at every level today).
3990 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3991 {
3992 self.advance(); // TRANSACTION
3993 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3994 return Ok(Statement::SetTransaction { isolation: level });
3995 }
3996 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
3997 // alias — same accept-as-no-op as SET NAMES.
3998 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
3999 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4000 {
4001 self.advance(); // CHARACTER
4002 self.advance(); // SET
4003 if matches!(
4004 self.peek(),
4005 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4006 ) {
4007 self.advance();
4008 }
4009 return Ok(Statement::Empty);
4010 }
4011 // v7.39 (GUC) — PG spells the timezone GUC as two
4012 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4013 // where <value> is a string/ident or the LOCAL /
4014 // DEFAULT keyword (both mean "back to the default").
4015 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4016 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4017 {
4018 self.advance(); // TIME
4019 self.advance(); // ZONE
4020 let value = match self.peek().clone() {
4021 Token::Ident(s)
4022 if s.eq_ignore_ascii_case("local")
4023 || s.eq_ignore_ascii_case("default") =>
4024 {
4025 self.advance();
4026 crate::ast::SetValue::Default
4027 }
4028 Token::Default => {
4029 self.advance();
4030 crate::ast::SetValue::Default
4031 }
4032 _ => self.parse_set_value()?,
4033 };
4034 return Ok(Statement::SetParameter {
4035 name: "timezone".into(),
4036 value,
4037 local: set_local,
4038 });
4039 }
4040 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4041 // MySQL USER-variable assignment: its own per-session
4042 // namespace, an arbitrary expression on the right, and `:=`
4043 // as a second spelling of `=`. It used to fall into the
4044 // session-PARAMETER list below, whose values are literals and
4045 // whose store nothing reads back under a `@` name — so the
4046 // assignment reported success and vanished.
4047 //
4048 // A `@@`-prefixed LHS is a real engine setting and keeps the
4049 // old path.
4050 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4051 return self.parse_set_user_vars();
4052 }
4053 // v7.14.0 — multi-assignment form
4054 // `SET a = 1, b = 2, …`. Single-assignment is the
4055 // 1-element case. Each LHS may be a regular ident
4056 // or a SessionVar (`@VAR` / `@@VAR`).
4057 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4058 loop {
4059 let lhs = match self.peek().clone() {
4060 Token::SessionVar(s) => {
4061 self.advance();
4062 s
4063 }
4064 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4065 other => {
4066 return Err(self.err(format!(
4067 "expected parameter name after SET, got {other:?}"
4068 )));
4069 }
4070 };
4071 // Accept either `=` or the bare `TO` keyword.
4072 match self.peek() {
4073 Token::Eq => {
4074 self.advance();
4075 }
4076 Token::To => {
4077 self.advance();
4078 }
4079 other => {
4080 return Err(self.err(format!(
4081 "expected `=` or TO after SET {lhs}, got {other:?}"
4082 )));
4083 }
4084 }
4085 let mut value = self.parse_set_value()?;
4086 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4087 // `, name TO` continues a MySQL-style multi-assign,
4088 // anything else is a PG list VALUE
4089 // (`SET search_path = myschema, public`) folded into
4090 // one comma-joined string.
4091 while matches!(self.peek(), Token::Comma) {
4092 let is_assign = matches!(
4093 self.tokens.get(self.pos + 1),
4094 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4095 ) && matches!(
4096 self.tokens.get(self.pos + 2),
4097 Some(Token::Eq | Token::To)
4098 );
4099 if is_assign {
4100 break;
4101 }
4102 self.advance(); // comma
4103 let next = self.parse_set_value()?;
4104 let joined = alloc::format!(
4105 "{}, {}",
4106 set_value_text(&value),
4107 set_value_text(&next)
4108 );
4109 value = crate::ast::SetValue::String(joined);
4110 }
4111 pairs.push((lhs, value));
4112 if matches!(self.peek(), Token::Comma) {
4113 self.advance();
4114 continue;
4115 }
4116 break;
4117 }
4118 if pairs.len() == 1 {
4119 let (name, value) = pairs.into_iter().next().unwrap();
4120 Ok(Statement::SetParameter {
4121 name,
4122 value,
4123 local: set_local,
4124 })
4125 } else {
4126 Ok(Statement::SetParameterList(pairs))
4127 }
4128 }
4129 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4130 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4131 self.advance();
4132 match self.peek().clone() {
4133 Token::All => {
4134 self.advance();
4135 Ok(Statement::ResetParameter(None))
4136 }
4137 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4138 self.advance();
4139 Ok(Statement::ResetParameter(None))
4140 }
4141 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4142 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4143 self.advance();
4144 Ok(Statement::SetRole(None))
4145 }
4146 _ => {
4147 let name = self.parse_set_param_name()?;
4148 Ok(Statement::ResetParameter(Some(name)))
4149 }
4150 }
4151 }
4152 // v7.39 (round 218) — server-side cursors.
4153 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4154 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4155 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4156 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4157 self.advance();
4158 match self.peek().clone() {
4159 Token::All => {
4160 self.advance();
4161 Ok(Statement::CloseCursor { name: None })
4162 }
4163 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4164 self.advance();
4165 Ok(Statement::CloseCursor { name: None })
4166 }
4167 Token::Ident(n) | Token::QuotedIdent(n) => {
4168 self.advance();
4169 Ok(Statement::CloseCursor { name: Some(n) })
4170 }
4171 other => Err(self.err(format!(
4172 "expected cursor name or ALL after CLOSE, got {other:?}"
4173 ))),
4174 }
4175 }
4176 other => Err(self.err(format!(
4177 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4178 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4179 ))),
4180 }
4181 }
4182
4183 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4184 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4185 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4186 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4187 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4188 self.advance(); // DECLARE
4189 let name = match self.advance() {
4190 Token::Ident(n) | Token::QuotedIdent(n) => n,
4191 other => {
4192 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4193 }
4194 };
4195 let mut scroll: Option<bool> = None;
4196 loop {
4197 match self.peek() {
4198 Token::Ident(s)
4199 if s.eq_ignore_ascii_case("binary")
4200 || s.eq_ignore_ascii_case("insensitive")
4201 || s.eq_ignore_ascii_case("asensitive") =>
4202 {
4203 self.advance();
4204 }
4205 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4206 self.advance();
4207 scroll = Some(true);
4208 }
4209 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4210 {
4211 self.advance(); // NO
4212 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4213 return Err(self.err(format!(
4214 "expected SCROLL after NO in DECLARE, got {:?}",
4215 self.peek()
4216 )));
4217 }
4218 self.advance();
4219 scroll = Some(false);
4220 }
4221 _ => break,
4222 }
4223 }
4224 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4225 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4226 }
4227 self.advance();
4228 let mut hold = false;
4229 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4230 self.advance();
4231 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4232 return Err(self.err(format!(
4233 "expected HOLD after WITH in DECLARE, got {:?}",
4234 self.peek()
4235 )));
4236 }
4237 self.advance();
4238 hold = true;
4239 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4240 self.advance();
4241 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4242 return Err(self.err(format!(
4243 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4244 self.peek()
4245 )));
4246 }
4247 self.advance();
4248 }
4249 if !matches!(self.peek(), Token::For) {
4250 return Err(self.err(format!(
4251 "expected FOR before the cursor query, got {:?}",
4252 self.peek()
4253 )));
4254 }
4255 self.advance();
4256 let query = self.parse_one_statement()?;
4257 Ok(Statement::DeclareCursor {
4258 name,
4259 scroll,
4260 hold,
4261 query: alloc::boxed::Box::new(query),
4262 })
4263 }
4264
4265 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4266 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4267 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4268 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4269 use crate::ast::CursorDirection as D;
4270 self.advance(); // FETCH / MOVE
4271 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4272 let neg = if matches!(this.peek(), Token::Minus) {
4273 this.advance();
4274 true
4275 } else {
4276 false
4277 };
4278 match this.advance() {
4279 Token::Integer(v) => Ok(if neg { -v } else { v }),
4280 other => Err(this.err(format!("expected count, got {other:?}"))),
4281 }
4282 };
4283 let direction = match self.peek().clone() {
4284 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4285 self.advance();
4286 D::Next
4287 }
4288 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4289 self.advance();
4290 D::Prior
4291 }
4292 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4293 self.advance();
4294 D::First
4295 }
4296 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4297 self.advance();
4298 D::Last
4299 }
4300 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4301 self.advance();
4302 D::Absolute(signed_count(self)?)
4303 }
4304 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4305 self.advance();
4306 D::Relative(signed_count(self)?)
4307 }
4308 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4309 self.advance();
4310 match self.peek().clone() {
4311 Token::All => {
4312 self.advance();
4313 D::All
4314 }
4315 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4316 self.advance();
4317 D::All
4318 }
4319 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4320 _ => D::Next, // bare FORWARD = FORWARD 1
4321 }
4322 }
4323 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4324 self.advance();
4325 match self.peek().clone() {
4326 Token::All => {
4327 self.advance();
4328 D::BackwardAll
4329 }
4330 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4331 self.advance();
4332 D::BackwardAll
4333 }
4334 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4335 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4336 }
4337 }
4338 Token::All => {
4339 self.advance();
4340 D::All
4341 }
4342 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4343 self.advance();
4344 D::All
4345 }
4346 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4347 // Bare `FETCH <name>` — direction defaults to NEXT.
4348 _ => D::Next,
4349 };
4350 // Optional FROM / IN.
4351 if matches!(self.peek(), Token::From)
4352 || matches!(self.peek(), Token::In)
4353 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4354 {
4355 self.advance();
4356 }
4357 let name = match self.advance() {
4358 Token::Ident(n) | Token::QuotedIdent(n) => n,
4359 other => {
4360 return Err(self.err(format!("expected cursor name, got {other:?}")));
4361 }
4362 };
4363 Ok(if is_move {
4364 Statement::MoveCursor { name, direction }
4365 } else {
4366 Statement::FetchCursor { name, direction }
4367 })
4368 }
4369
4370 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4371 /// [(kind, …)] ON <col>, … FROM <table>`.
4372 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4373 self.advance(); // STATISTICS
4374 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4375 let mut if_not_exists = false;
4376 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4377 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4378 {
4379 self.advance();
4380 self.advance();
4381 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4382 self.advance();
4383 if_not_exists = true;
4384 }
4385 }
4386 let name = self.expect_ident_like()?;
4387 let mut kinds = Vec::new();
4388 if matches!(self.peek(), Token::LParen) {
4389 self.advance();
4390 loop {
4391 let k = self.expect_ident_like()?;
4392 // PG stores the single letters; accept the spelled-out
4393 // names the SQL uses and record what PG records.
4394 kinds.push(match k.to_ascii_lowercase().as_str() {
4395 "ndistinct" => String::from("d"),
4396 "dependencies" => String::from("f"),
4397 "mcv" => String::from("m"),
4398 other => {
4399 return Err(
4400 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4401 );
4402 }
4403 });
4404 match self.advance() {
4405 Token::Comma => {}
4406 Token::RParen => break,
4407 other => {
4408 return Err(self.err(alloc::format!(
4409 "expected ',' or ')' in statistics kind list, got {other:?}"
4410 )));
4411 }
4412 }
4413 }
4414 }
4415 if !matches!(self.peek(), Token::On) {
4416 return Err(self.err(alloc::format!(
4417 "expected ON in CREATE STATISTICS, got {:?}",
4418 self.peek()
4419 )));
4420 }
4421 self.advance();
4422 let mut columns = Vec::new();
4423 loop {
4424 columns.push(self.expect_ident_like()?);
4425 if matches!(self.peek(), Token::Comma) {
4426 self.advance();
4427 } else {
4428 break;
4429 }
4430 }
4431 if !matches!(self.peek(), Token::From) {
4432 return Err(self.err(alloc::format!(
4433 "expected FROM in CREATE STATISTICS, got {:?}",
4434 self.peek()
4435 )));
4436 }
4437 self.advance();
4438 let table = self.expect_ident_like()?;
4439 Ok(Statement::CreateStatistics {
4440 name,
4441 if_not_exists,
4442 kinds,
4443 columns,
4444 table,
4445 })
4446 }
4447
4448 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4449 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4450 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4451 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4452 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4453 /// forward call.
4454 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4455 self.advance(); // TABLE
4456 let if_exists = self.consume_if_exists();
4457 let mut names: Vec<String> = Vec::new();
4458 loop {
4459 names.push(self.expect_ident_like()?);
4460 if matches!(self.peek(), Token::Comma) {
4461 self.advance();
4462 continue;
4463 }
4464 break;
4465 }
4466 if matches!(
4467 self.peek(),
4468 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4469 || s.eq_ignore_ascii_case("restrict")
4470 ) {
4471 self.advance();
4472 }
4473 Ok(Statement::DropTable { names, if_exists })
4474 }
4475
4476 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4477 self.advance(); // STATISTICS
4478 let mut if_exists = false;
4479 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4480 && matches!(self.tokens.get(self.pos + 1),
4481 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4482 {
4483 self.advance();
4484 self.advance();
4485 if_exists = true;
4486 }
4487 let name = self.expect_ident_like()?;
4488 Ok(Statement::DropStatistics { name, if_exists })
4489 }
4490
4491 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4492 debug_assert!(matches!(self.peek(), Token::Create));
4493 self.advance();
4494 match self.peek() {
4495 Token::Table => self.parse_create_table_stmt_after_create(),
4496 Token::Index => self.parse_create_index_stmt_after_create(false),
4497 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4498 // object now. It used to be consumed by the CREATE-noise
4499 // arm, so a pg_dump that declares extended statistics
4500 // restored silently without them and reflection showed
4501 // nothing.
4502 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4503 self.parse_create_statistics_after_create()
4504 }
4505 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4506 // The `UNIQUE` modifier turns a partial index into a
4507 // partial-uniqueness invariant (only rows matching the
4508 // WHERE predicate are checked for duplicates). mailrs
4509 // K1 (3 hits: email_templates default, calendar_events
4510 // master, calendar_events instance).
4511 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4512 self.advance();
4513 if !matches!(self.peek(), Token::Index) {
4514 return Err(self.err(alloc::format!(
4515 "expected INDEX after CREATE UNIQUE, got {:?}",
4516 self.peek()
4517 )));
4518 }
4519 self.parse_create_index_stmt_after_create(true)
4520 }
4521 Token::Publication => {
4522 self.advance();
4523 self.parse_create_publication_after_keyword()
4524 }
4525 Token::Subscription => {
4526 self.advance();
4527 self.parse_create_subscription_after_keyword()
4528 }
4529 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4530 // USER isn't a reserved keyword — we look for the bare
4531 // identifier so the lexer doesn't have to grow a token.
4532 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4533 self.advance();
4534 self.parse_create_user_after_keyword(true)
4535 }
4536 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4537 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4538 // the default of the LOGIN attribute.
4539 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4540 self.advance();
4541 self.parse_create_user_after_keyword(false)
4542 }
4543 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4544 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4545 self.advance();
4546 self.parse_create_policy_after_keyword()
4547 }
4548 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4549 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4550 // no-op. mailrs follow-up F3.
4551 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4552 self.advance();
4553 self.parse_create_extension_after_keyword()
4554 }
4555 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4556 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4557 // optional; absorb it here and forward to the
4558 // per-kind parsers with the flag. OR is a reserved
4559 // keyword token.
4560 Token::Or => {
4561 self.advance();
4562 let next = self.peek();
4563 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4564 return Err(self.err(alloc::format!(
4565 "expected REPLACE after CREATE OR, got {next:?}"
4566 )));
4567 };
4568 if !s2.eq_ignore_ascii_case("replace") {
4569 return Err(self.err(alloc::format!(
4570 "expected REPLACE after CREATE OR, got {s2:?}"
4571 )));
4572 }
4573 self.advance();
4574 self.parse_create_function_or_trigger_after_or_replace(true)
4575 }
4576 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4577 self.advance();
4578 self.parse_create_function_after_keyword(false)
4579 }
4580 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4581 self.advance();
4582 self.parse_create_trigger_after_keyword(false)
4583 }
4584 // v7.39 (round 139) — CREATE RULE …
4585 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4586 self.advance();
4587 self.parse_create_rule_after_keyword(false)
4588 }
4589 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4590 // trigger is a row-level AFTER trigger that additionally carries
4591 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4592 // path already tolerates and skips those clauses, so consuming the
4593 // CONSTRAINT keyword and reusing it makes the statement parse and the
4594 // trigger fire. (The deferral timing itself is not yet honoured —
4595 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4596 // for every non-deferred use.)
4597 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4598 self.advance();
4599 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4600 if t.eq_ignore_ascii_case("trigger"))
4601 {
4602 return Err(self.err(alloc::format!(
4603 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4604 self.peek()
4605 )));
4606 }
4607 self.advance();
4608 self.parse_create_trigger_after_keyword(false)
4609 }
4610 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4611 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4612 self.advance();
4613 self.parse_create_sequence_after_keyword(false)
4614 }
4615 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4616 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4617 self.advance();
4618 self.parse_create_view_after_keyword(false, false, false)
4619 }
4620 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4621 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4622 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4623 // appear (in any order) between `CREATE` and `VIEW` in
4624 // every mysqldump-emitted view. Pre-2.6 the parser
4625 // rejected the prefix and the customer's whole view
4626 // backup failed on the first view. The hints are pure
4627 // planner / permission metadata; SPG's view-rewrite
4628 // path is semantically equivalent for all three
4629 // algorithms in v7.17 (TEMPTABLE differs only in
4630 // perf for huge views — out of v7.17 scope), and
4631 // DEFINER / SQL SECURITY are pure single-user
4632 // permissioning that SPG ignores by design.
4633 Token::Ident(s) | Token::QuotedIdent(s)
4634 if s.eq_ignore_ascii_case("algorithm")
4635 || s.eq_ignore_ascii_case("definer")
4636 || s.eq_ignore_ascii_case("sql") =>
4637 {
4638 self.consume_mysql_view_prefix()?;
4639 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4640 // (in any order, in any combination), the next
4641 // keyword must be VIEW. mysqldump never emits these
4642 // prefixes on non-view statements.
4643 let next = self.peek().clone();
4644 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4645 if s2.eq_ignore_ascii_case("view"))
4646 {
4647 self.advance();
4648 self.parse_create_view_after_keyword(false, false, false)
4649 } else {
4650 Err(self.err(alloc::format!(
4651 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4652 )))
4653 }
4654 }
4655 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4656 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4657 self.advance();
4658 self.parse_create_type_after_keyword()
4659 }
4660 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4661 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4662 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4663 self.advance();
4664 self.parse_create_domain_after_keyword()
4665 }
4666 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4667 // name [AUTHORIZATION user]. Real catalog registry
4668 // (was silent-no-op'd pre-v7.17).
4669 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4670 self.advance();
4671 let if_not_exists = self.parse_if_not_exists();
4672 let name = self.expect_ident_like()?;
4673 // Optional `AUTHORIZATION <user>` trailer — accepted,
4674 // ignored (single-user catalog).
4675 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4676 if s.eq_ignore_ascii_case("authorization"))
4677 {
4678 self.advance();
4679 let _ = self.expect_ident_like()?;
4680 }
4681 Ok(Statement::CreateSchema { name, if_not_exists })
4682 }
4683 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4684 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4685 self.advance();
4686 let next = self.peek().clone();
4687 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4688 {
4689 self.advance();
4690 self.parse_create_materialized_view_after_keyword()
4691 } else {
4692 Err(self.err(alloc::format!(
4693 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4694 )))
4695 }
4696 }
4697 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4698 // no-op below), an UNLOGGED table is a real, fully-usable table in
4699 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4700 // durability optimisation is a follow-up), so a dump / app that
4701 // declares UNLOGGED tables works instead of failing to parse.
4702 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4703 self.advance(); // UNLOGGED
4704 if matches!(self.peek(), Token::Table) {
4705 self.parse_create_table_stmt_after_create()
4706 } else {
4707 Err(self.err(format!(
4708 "expected TABLE after CREATE UNLOGGED, got {:?}",
4709 self.peek()
4710 )))
4711 }
4712 }
4713 Token::Ident(s) | Token::QuotedIdent(s)
4714 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4715 {
4716 self.advance();
4717 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4718 let next = self.peek().clone();
4719 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4720 {
4721 self.advance();
4722 self.parse_create_sequence_after_keyword(true)
4723 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4724 {
4725 self.advance();
4726 self.parse_create_view_after_keyword(false, false, true)
4727 } else {
4728 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4729 // consumed and answered OK while creating nothing, so
4730 // every statement that touched the table afterwards failed
4731 // with "table not found" — the DDL itself lied. It is a
4732 // real CREATE TABLE now, marked temporary so the executor
4733 // puts it in the session's own namespace. An optional
4734 // TABLE keyword may or may not be present (`CREATE TEMP t`
4735 // is not legal, but the keyword is consumed by the
4736 // CREATE TABLE parser itself).
4737 let stmt = self.parse_create_table_stmt_after_create()?;
4738 match stmt {
4739 Statement::CreateTable(mut c) => {
4740 c.temporary = true;
4741 Ok(Statement::CreateTable(c))
4742 }
4743 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4744 // CTAS node, which needs the same session namespace.
4745 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4746 m.temporary = true;
4747 Ok(Statement::CreateMaterializedView(m))
4748 }
4749 other => Ok(other),
4750 }
4751 }
4752 }
4753 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4754 // BEGIN <body> END`. The body may reference `@var`
4755 // session variables, SET statements, internal `;`
4756 // terminators, etc. SPG has no procedure runtime, so
4757 // consume the whole `CREATE PROCEDURE … END` block as
4758 // a no-op so mysqldump scripts that include stored
4759 // routines load through. The matching-END consumer
4760 // tracks BEGIN/END nesting depth to handle nested
4761 // BEGIN blocks correctly.
4762 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4763 self.consume_mysql_routine_body();
4764 Ok(Statement::Empty)
4765 }
4766 // v7.14.0 — pg_dump / mysqldump emit
4767 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4768 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4769 // SPG is single-schema / single-database; these have
4770 // no behavioural effect, so consume + return Empty.
4771 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4772 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4773 // moved up to real parser branches. DATABASE / ROLE /
4774 // POLICY / OPERATOR stay no-op forever
4775 // (single-database, hardcoded roles).
4776 Token::Ident(s) | Token::QuotedIdent(s)
4777 if matches!(
4778 s.to_ascii_lowercase().as_str(),
4779 "database"
4780 | "role"
4781 | "operator"
4782 | "cast"
4783 | "aggregate"
4784 | "language"
4785 | "collation"
4786 | "conversion"
4787 // v7.17.0 Phase 8 (audit N6) — rarely-
4788 // emitted pg_dump shapes that should
4789 // load through without a parser error.
4790 // SPG has no planner statistics catalog,
4791 // no event-trigger hooks, no foreign-
4792 // data-wrapper infrastructure; consume
4793 // + return Empty.
4794 | "statistics"
4795 | "event"
4796 // v7.37.17 (17.6 siblings) — additional CREATE
4797 // targets pg_dump / operator install scripts
4798 // may emit that SPG has no matching machinery
4799 // for. Consume + Empty-return.
4800 | "text"
4801 | "tablespace"
4802 | "access"
4803 | "large"
4804 ) =>
4805 {
4806 // DATABASE is the one member of this list PG refuses
4807 // inside a transaction block; the rest (ROLE, CAST,
4808 // TABLESPACE, …) it runs there quite happily, so only
4809 // this one is named. Still a no-op otherwise — SPG is
4810 // single-database.
4811 let is_database = s.eq_ignore_ascii_case("database");
4812 self.consume_until_statement_boundary();
4813 if is_database {
4814 return Ok(Statement::NoOpPreventedInTransaction {
4815 what: String::from("CREATE DATABASE"),
4816 });
4817 }
4818 Ok(Statement::Empty)
4819 }
4820 // v7.39 (round 706) — the foreign-data family leaves the silent
4821 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4822 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4823 // FDW machinery), but the ENGINE now warns, so a restore log
4824 // says what will not function instead of reporting success.
4825 Token::Ident(s) | Token::QuotedIdent(s)
4826 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4827 {
4828 self.consume_until_statement_boundary();
4829 Ok(Statement::ValidateOnly {
4830 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4831 names: Vec::new(),
4832 })
4833 }
4834 other => Err(self.err(format!(
4835 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4836 ))),
4837 }
4838 }
4839
4840 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4841 /// keyword decides whether we parse a function or trigger
4842 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4843 /// PROCEDURE) — those land in later releases.
4844 fn parse_create_function_or_trigger_after_or_replace(
4845 &mut self,
4846 or_replace: bool,
4847 ) -> Result<Statement, ParseError> {
4848 let tok = self.peek();
4849 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4850 return Err(self.err(alloc::format!(
4851 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4852 )));
4853 };
4854 if s.eq_ignore_ascii_case("function") {
4855 self.advance();
4856 self.parse_create_function_after_keyword(or_replace)
4857 } else if s.eq_ignore_ascii_case("trigger") {
4858 self.advance();
4859 self.parse_create_trigger_after_keyword(or_replace)
4860 } else if s.eq_ignore_ascii_case("rule") {
4861 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4862 self.advance();
4863 self.parse_create_rule_after_keyword(or_replace)
4864 } else if s.eq_ignore_ascii_case("view") {
4865 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4866 self.advance();
4867 self.parse_create_view_after_keyword(or_replace, false, false)
4868 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4869 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4870 self.advance();
4871 let nxt = self.peek().clone();
4872 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4873 {
4874 self.advance();
4875 self.parse_create_view_after_keyword(or_replace, false, true)
4876 } else {
4877 Err(self.err(alloc::format!(
4878 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4879 )))
4880 }
4881 } else {
4882 Err(self.err(alloc::format!(
4883 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4884 )))
4885 }
4886 }
4887
4888 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4889 /// SPG doesn't have a registry; pgvector / similar are
4890 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4891 /// the syntax lets dual-target schemas keep the line.
4892 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4893 // Optional `IF NOT EXISTS`.
4894 self.consume_if_not_exists();
4895 let name = self.expect_ident_like()?;
4896 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4897 // CASCADE / FROM '<v>' clauses; we don't model them.
4898 loop {
4899 match self.peek() {
4900 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4901 self.advance();
4902 continue;
4903 }
4904 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4905 self.advance();
4906 let _ = self.expect_ident_like()?;
4907 continue;
4908 }
4909 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4910 self.advance();
4911 // String or ident literal.
4912 let _ = self.advance();
4913 continue;
4914 }
4915 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4916 self.advance();
4917 let _ = self.advance();
4918 continue;
4919 }
4920 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4921 self.advance();
4922 continue;
4923 }
4924 _ => break,
4925 }
4926 }
4927 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4928 // nosuch` reported success and `pg_extension` then did not list it,
4929 // which is the accept-and-do-nothing shape F31 exists to find.
4930 Ok(Statement::ValidateOnly {
4931 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4932 names: alloc::vec![name],
4933 })
4934 }
4935
4936 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4937 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4938 /// already been consumed by the caller. Grammar accepted:
4939 ///
4940 /// name `(` arg-list `)`
4941 /// `RETURNS` return-type
4942 /// [ `LANGUAGE` ident ]
4943 /// `AS` $$ body $$
4944 /// [ `LANGUAGE` ident ]
4945 ///
4946 /// Either `LANGUAGE` position is allowed; PG accepts both.
4947 fn parse_create_function_after_keyword(
4948 &mut self,
4949 or_replace: bool,
4950 ) -> Result<Statement, ParseError> {
4951 let name = self.expect_ident_like()?;
4952 // Argument list. v7.12.4 commonly sees the empty `()`
4953 // (trigger functions); typed args parse and round-trip
4954 // but the executor only invokes nullary functions.
4955 if !matches!(self.peek(), Token::LParen) {
4956 return Err(self.err(alloc::format!(
4957 "expected '(' after function name {name:?}, got {:?}",
4958 self.peek()
4959 )));
4960 }
4961 self.advance();
4962 let args = self.parse_function_arg_list()?;
4963 // RETURNS clause.
4964 let tok = self.peek();
4965 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4966 return Err(self.err(alloc::format!(
4967 "expected RETURNS after function arg list, got {tok:?}"
4968 )));
4969 };
4970 if !s.eq_ignore_ascii_case("returns") {
4971 return Err(self.err(alloc::format!(
4972 "expected RETURNS after function arg list, got {s:?}"
4973 )));
4974 }
4975 self.advance();
4976 let returns = self.parse_function_return()?;
4977 // Optional LANGUAGE clause (PG also accepts after AS — we'll
4978 // re-check after the body too).
4979 let mut language: Option<String> = self.parse_optional_language()?;
4980 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4981 // either side of the body and in any order, interleaved with
4982 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4983 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4984 // PG's own pg_dump output did not restore.
4985 let mut attrs = FunctionAttrs::default();
4986 loop {
4987 let before = self.pos;
4988 self.parse_function_attrs_into(&mut attrs)?;
4989 if language.is_none() {
4990 language = self.parse_optional_language()?;
4991 }
4992 if self.pos == before {
4993 break;
4994 }
4995 }
4996 // `AS` followed by a $$-quoted body (lexer already
4997 // collapses both `$$…$$` and `$tag$…$tag$` to a single
4998 // Token::String). AS is a reserved keyword (Token::As).
4999 if !matches!(self.peek(), Token::As) {
5000 return Err(self.err(alloc::format!(
5001 "expected AS before function body, got {:?}",
5002 self.peek()
5003 )));
5004 }
5005 self.advance();
5006 let body_text = match self.peek() {
5007 Token::String(s) => {
5008 let body = s.clone();
5009 self.advance();
5010 body
5011 }
5012 other => {
5013 return Err(self.err(alloc::format!(
5014 "expected $$-quoted function body after AS, got {other:?}"
5015 )));
5016 }
5017 };
5018 // Trailing clauses — PG's other accepted position for both the
5019 // LANGUAGE and the attributes.
5020 loop {
5021 let before = self.pos;
5022 self.parse_function_attrs_into(&mut attrs)?;
5023 if language.is_none() {
5024 language = self.parse_optional_language()?;
5025 }
5026 if self.pos == before {
5027 break;
5028 }
5029 }
5030 let language = language.unwrap_or_else(|| String::from("sql"));
5031 // PL/pgSQL bodies get structure-parsed. Other languages
5032 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5033 // recognise) round-trip as Raw text — the executor errors
5034 // when invoked with a clear unsupported message.
5035 let body = if language.eq_ignore_ascii_case("plpgsql") {
5036 match parse_plpgsql_body(&body_text) {
5037 Ok(block) => FunctionBody::PlPgSql(block),
5038 // Best-effort: if the body parser doesn't yet
5039 // support a construct used inside, fall back to
5040 // raw — keeps `CREATE FUNCTION` itself working
5041 // (catalogue accepts), executor errors on
5042 // invocation only.
5043 Err(_) => FunctionBody::Raw(body_text),
5044 }
5045 } else {
5046 FunctionBody::Raw(body_text)
5047 };
5048 Ok(Statement::CreateFunction(CreateFunctionStatement {
5049 name,
5050 or_replace,
5051 args,
5052 returns,
5053 language,
5054 body,
5055 attrs,
5056 }))
5057 }
5058
5059 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5060 /// attribute clauses into `attrs`, stopping at the first token that
5061 /// is not one. Measured against PG 18.4, which accepts them in any
5062 /// order and on either side of the body.
5063 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5064 loop {
5065 let word = match self.peek() {
5066 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5067 // NOT LEAKPROOF — NOT is a reserved keyword token.
5068 Token::Not
5069 if matches!(
5070 self.tokens.get(self.pos + 1),
5071 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5072 ) =>
5073 {
5074 self.advance();
5075 self.advance();
5076 attrs.leakproof = false;
5077 continue;
5078 }
5079 _ => return Ok(()),
5080 };
5081 match word.as_str() {
5082 "immutable" => {
5083 self.advance();
5084 attrs.volatility = FunctionVolatility::Immutable;
5085 }
5086 "stable" => {
5087 self.advance();
5088 attrs.volatility = FunctionVolatility::Stable;
5089 }
5090 "volatile" => {
5091 self.advance();
5092 attrs.volatility = FunctionVolatility::Volatile;
5093 }
5094 "strict" => {
5095 self.advance();
5096 attrs.strict = true;
5097 }
5098 "leakproof" => {
5099 self.advance();
5100 attrs.leakproof = true;
5101 }
5102 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5103 // spelled-out forms of STRICT and its opposite.
5104 "returns" | "called" => {
5105 let strict = word == "returns";
5106 let mut probe = self.pos + 1;
5107 if strict {
5108 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5109 // is not ours.
5110 match self.tokens.get(probe) {
5111 Some(Token::Null) => probe += 1,
5112 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5113 _ => return Ok(()),
5114 }
5115 }
5116 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5117 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5118 if !ok {
5119 return Ok(());
5120 }
5121 probe += 1;
5122 match self.tokens.get(probe) {
5123 Some(Token::Null) => probe += 1,
5124 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5125 _ => return Ok(()),
5126 }
5127 match self.tokens.get(probe) {
5128 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5129 _ => return Ok(()),
5130 }
5131 self.pos = probe;
5132 attrs.strict = strict;
5133 }
5134 "security" | "external" => {
5135 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5136 let mut probe = self.pos + 1;
5137 if word == "external" {
5138 match self.tokens.get(probe) {
5139 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5140 probe += 1;
5141 }
5142 _ => return Ok(()),
5143 }
5144 }
5145 let definer = match self.tokens.get(probe) {
5146 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5147 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5148 _ => return Ok(()),
5149 };
5150 self.pos = probe + 1;
5151 attrs.security_definer = definer;
5152 }
5153 "parallel" => {
5154 let level = match self.tokens.get(self.pos + 1) {
5155 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5156 FunctionParallel::Safe
5157 }
5158 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5159 FunctionParallel::Restricted
5160 }
5161 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5162 FunctionParallel::Unsafe
5163 }
5164 _ => return Ok(()),
5165 };
5166 self.pos += 2;
5167 attrs.parallel = level;
5168 }
5169 "cost" | "rows" => {
5170 let Some(n) = self.peek_number_at(self.pos + 1) else {
5171 return Ok(());
5172 };
5173 self.pos += 2;
5174 if word == "cost" {
5175 attrs.cost = Some(n);
5176 } else {
5177 attrs.rows = Some(n);
5178 }
5179 }
5180 _ => return Ok(()),
5181 }
5182 }
5183 }
5184
5185 /// The numeric literal at `idx`, if there is one.
5186 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5187 match self.tokens.get(idx)? {
5188 Token::Integer(n) => Some(*n as f64),
5189 Token::Float(f) => Some(*f),
5190 Token::Numeric(t) => t.parse::<f64>().ok(),
5191 _ => None,
5192 }
5193 }
5194
5195 /// Closing `)`-terminated argument list. v7.12.4 commonly
5196 /// sees the empty `()`; typed args round-trip but the
5197 /// executor (yet) doesn't invoke them.
5198 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5199 /// it away, which is what PG does with one on a function parameter.
5200 fn skip_type_modifier(&mut self) {
5201 if !matches!(self.peek(), Token::LParen) {
5202 return;
5203 }
5204 // Only a numeric modifier — anything else is not one, and eating
5205 // it would swallow real grammar.
5206 let mut i = self.pos + 1;
5207 let mut seen_number = false;
5208 loop {
5209 match self.tokens.get(i) {
5210 Some(Token::Integer(_)) => seen_number = true,
5211 Some(Token::Comma) => {}
5212 Some(Token::RParen) => break,
5213 _ => return,
5214 }
5215 i += 1;
5216 }
5217 if !seen_number {
5218 return;
5219 }
5220 while self.pos <= i {
5221 self.advance();
5222 }
5223 }
5224
5225 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5226 let mut args: Vec<FunctionArg> = Vec::new();
5227 if matches!(self.peek(), Token::RParen) {
5228 self.advance();
5229 return Ok(args);
5230 }
5231 loop {
5232 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5233 // a reserved token; OUT / INOUT are bare idents.
5234 let mode = if matches!(self.peek(), Token::In) {
5235 self.advance();
5236 FunctionArgMode::In
5237 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5238 {
5239 self.advance();
5240 FunctionArgMode::Out
5241 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5242 {
5243 self.advance();
5244 FunctionArgMode::InOut
5245 } else {
5246 FunctionArgMode::In
5247 };
5248 // Optional name. The next token is either a name
5249 // (followed by a type ident) or the type itself.
5250 // Disambiguate by peeking ahead: if the token after
5251 // the next ident is also an ident, we treat the
5252 // first as the name.
5253 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5254 // the comma or paren, then decide. Reading at most two of
5255 // them could not spell `x double precision` at all, and
5256 // silently mis-read the bare `double precision` as a
5257 // parameter named "double" — which is what made the same
5258 // signature key two different ways.
5259 let (name, ty_token) = {
5260 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5261 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5262 words.push(self.expect_ident_like()?);
5263 }
5264 // v7.39 (round 344) — a length / precision modifier on the
5265 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5266 // accepts it and DROPS it — `pg_get_function_arguments`
5267 // reports plain `character varying` / `numeric`, measured on
5268 // 18.4 — but SPG raised `syntax error at or near "("`,
5269 // because the modifier's parens were never consumed.
5270 self.skip_type_modifier();
5271 let whole = words.join(" ");
5272 if words.len() >= 2 && !is_multiword_type_phrase(&whole) {
5273 (Some(words[0].clone()), words[1..].join(" "))
5274 } else {
5275 (None, whole)
5276 }
5277 };
5278 // Type — try to map to ColumnTypeName, else Raw.
5279 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5280 Some(t) => FunctionArgType::Typed(t),
5281 None => FunctionArgType::Raw(ty_token),
5282 };
5283 args.push(FunctionArg { mode, name, ty });
5284 match self.peek() {
5285 Token::Comma => {
5286 self.advance();
5287 continue;
5288 }
5289 Token::RParen => {
5290 self.advance();
5291 return Ok(args);
5292 }
5293 other => {
5294 return Err(self.err(alloc::format!(
5295 "expected , or ) in function arg list, got {other:?}"
5296 )));
5297 }
5298 }
5299 }
5300 }
5301
5302 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5303 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5304 // function whose row shape is named inline.
5305 if matches!(self.peek(), Token::Table)
5306 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5307 {
5308 self.advance(); // TABLE
5309 self.advance(); // (
5310 let mut cols: Vec<String> = Vec::new();
5311 loop {
5312 let cname = self.expect_ident_like()?;
5313 let mut ty: Vec<String> = Vec::new();
5314 loop {
5315 match self.peek() {
5316 Token::Comma | Token::RParen | Token::Eof => break,
5317 _ => {}
5318 }
5319 match self.advance() {
5320 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5321 other => {
5322 if let Some(w) = unreserved_keyword_text(&other) {
5323 ty.push(w);
5324 }
5325 }
5326 }
5327 }
5328 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5329 if matches!(self.peek(), Token::Comma) {
5330 self.advance();
5331 } else {
5332 break;
5333 }
5334 }
5335 if matches!(self.peek(), Token::RParen) {
5336 self.advance();
5337 }
5338 return Ok(FunctionReturn::Other(alloc::format!(
5339 "TABLE({})",
5340 cols.join(", ")
5341 )));
5342 }
5343 let ident = self.expect_ident_like()?;
5344 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5345 if ident.eq_ignore_ascii_case("setof") {
5346 let inner = self.expect_ident_like()?;
5347 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5348 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5349 }
5350 if ident.eq_ignore_ascii_case("trigger") {
5351 return Ok(FunctionReturn::Trigger);
5352 }
5353 if ident.eq_ignore_ascii_case("void") {
5354 return Ok(FunctionReturn::Void);
5355 }
5356 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5357 // RETURN position did not, so the `[` was a syntax error and the
5358 // whole migration stopped. sentori worked around it by returning
5359 // zero-padded text.
5360 let suffix = self.consume_array_suffix();
5361 if !suffix.is_empty() {
5362 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5363 }
5364 match map_type_ident_to_column_type_name(&ident) {
5365 Some(t) => Ok(FunctionReturn::Type(t)),
5366 None => Ok(FunctionReturn::Other(ident)),
5367 }
5368 }
5369
5370 /// Consume any `[]` / `[N]` array markers after a type name and give
5371 /// back their text. Empty when there are none.
5372 fn consume_array_suffix(&mut self) -> String {
5373 let mut out = String::new();
5374 while matches!(self.peek(), Token::LBracket) {
5375 self.advance();
5376 // `[N]` is accepted and, as in PG, the length is not enforced.
5377 if let Token::Integer(n) = self.peek().clone() {
5378 self.advance();
5379 out.push_str(&alloc::format!("[{n}]"));
5380 } else {
5381 out.push_str("[]");
5382 }
5383 if matches!(self.peek(), Token::RBracket) {
5384 self.advance();
5385 }
5386 }
5387 out
5388 }
5389
5390 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5391 match self.peek() {
5392 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5393 self.advance();
5394 let lang = self.expect_ident_like()?;
5395 Ok(Some(lang.to_ascii_lowercase()))
5396 }
5397 _ => Ok(None),
5398 }
5399 }
5400
5401 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5402 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5403 /// (expr)]*`. The `DOMAIN` keyword has already been
5404 /// consumed. PG allows the trailing constraints in any
5405 /// order; we approximate with a small loop.
5406 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5407 let name = self.expect_ident_like()?;
5408 // Optional `AS`.
5409 if matches!(self.peek(), Token::As) {
5410 self.advance();
5411 }
5412 // v7.39 (round 259) — keep the raw type NAME when the base is not
5413 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5414 // parent domain.
5415 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5416 self.parse_type_with_implied_flags()?;
5417 let mut default: Option<Expr> = None;
5418 let mut not_null = false;
5419 let mut checks: Vec<Expr> = Vec::new();
5420 loop {
5421 match self.peek() {
5422 Token::Default => {
5423 if default.is_some() {
5424 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5425 }
5426 self.advance();
5427 default = Some(self.parse_expr(0)?);
5428 }
5429 Token::Not => {
5430 self.advance();
5431 if !matches!(self.peek(), Token::Null) {
5432 return Err(self.err(alloc::format!(
5433 "expected NULL after NOT in DOMAIN, got {:?}",
5434 self.peek()
5435 )));
5436 }
5437 self.advance();
5438 not_null = true;
5439 }
5440 Token::Null => {
5441 self.advance();
5442 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5443 // is the default-nullable marker (PG accepts it),
5444 // but AFTER a NOT NULL it is a conflict PG refuses
5445 // (`conflicting NULL/NOT NULL constraints`,
5446 // PG18-measured); the old arm no-opped both ways.
5447 if not_null {
5448 return Err(self.err(alloc::string::String::from(
5449 "conflicting NULL/NOT NULL constraints",
5450 )));
5451 }
5452 }
5453 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5454 self.advance();
5455 if !matches!(self.peek(), Token::LParen) {
5456 return Err(self.err(alloc::format!(
5457 "expected '(' after CHECK in DOMAIN, got {:?}",
5458 self.peek()
5459 )));
5460 }
5461 self.advance();
5462 let expr = self.parse_expr(0)?;
5463 if !matches!(self.peek(), Token::RParen) {
5464 return Err(self.err(alloc::format!(
5465 "expected ')' after CHECK expr, got {:?}",
5466 self.peek()
5467 )));
5468 }
5469 self.advance();
5470 checks.push(expr);
5471 }
5472 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5473 // prefix on the constraint; we drop the name and
5474 // recurse into the constraint parsing.
5475 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5476 self.advance();
5477 let _ = self.expect_ident_like()?;
5478 }
5479 _ => break,
5480 }
5481 }
5482 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5483 name,
5484 base_type,
5485 base_domain: base_user_ref,
5486 default,
5487 not_null,
5488 checks,
5489 }))
5490 }
5491
5492 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5493 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5494 /// consumed.
5495 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5496 let name = self.expect_ident_like()?;
5497 // Required `AS`.
5498 if !matches!(self.peek(), Token::As) {
5499 return Err(self.err(alloc::format!(
5500 "expected AS after CREATE TYPE {name:?}, got {:?}",
5501 self.peek()
5502 )));
5503 }
5504 self.advance();
5505 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5506 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5507 // on the next token: `(` = composite, ident `ENUM` = enum.
5508 if matches!(self.peek(), Token::LParen) {
5509 self.advance();
5510 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5511 let mut field_user_types: Vec<Option<String>> = Vec::new();
5512 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5513 // is legal PG (an attribute-less composite; measured — the old
5514 // e2e note claimed PG requires at least one attribute).
5515 if matches!(self.peek(), Token::RParen) {
5516 self.advance();
5517 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5518 name,
5519 kind: crate::ast::TypeKind::Composite {
5520 fields,
5521 field_user_types,
5522 },
5523 }));
5524 }
5525 loop {
5526 let field_name = self.expect_ident_like()?;
5527 // v7.39 (round 264) — keep the raw type name when it is not
5528 // a builtin: that is how a NESTED composite field records
5529 // which composite it holds.
5530 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5531 self.parse_type_with_implied_flags()?;
5532 fields.push((field_name, field_type));
5533 field_user_types.push(field_user_ref);
5534 if matches!(self.peek(), Token::Comma) {
5535 self.advance();
5536 continue;
5537 }
5538 if matches!(self.peek(), Token::RParen) {
5539 self.advance();
5540 break;
5541 }
5542 return Err(self.err(alloc::format!(
5543 "expected , or ) in composite field list, got {:?}",
5544 self.peek()
5545 )));
5546 }
5547 if fields.is_empty() {
5548 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5549 }
5550 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5551 name,
5552 kind: crate::ast::TypeKind::Composite {
5553 fields,
5554 field_user_types,
5555 },
5556 }));
5557 }
5558 // Required `ENUM` ident.
5559 let kind_ident = match self.peek().clone() {
5560 Token::Ident(s) | Token::QuotedIdent(s) => s,
5561 other => {
5562 return Err(self.err(alloc::format!(
5563 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5564 )));
5565 }
5566 };
5567 if !kind_ident.eq_ignore_ascii_case("enum") {
5568 return Err(self.err(alloc::format!(
5569 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5570 )));
5571 }
5572 self.advance();
5573 if !matches!(self.peek(), Token::LParen) {
5574 return Err(self.err(alloc::format!(
5575 "expected '(' after ENUM, got {:?}",
5576 self.peek()
5577 )));
5578 }
5579 self.advance();
5580 let mut labels: Vec<String> = Vec::new();
5581 loop {
5582 match self.peek().clone() {
5583 Token::String(s) => {
5584 self.advance();
5585 labels.push(s);
5586 }
5587 other => {
5588 return Err(
5589 self.err(alloc::format!("expected enum label string, got {other:?}"))
5590 );
5591 }
5592 }
5593 if matches!(self.peek(), Token::Comma) {
5594 self.advance();
5595 continue;
5596 }
5597 if matches!(self.peek(), Token::RParen) {
5598 self.advance();
5599 break;
5600 }
5601 return Err(self.err(alloc::format!(
5602 "expected , or ) in ENUM label list, got {:?}",
5603 self.peek()
5604 )));
5605 }
5606 if labels.is_empty() {
5607 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5608 }
5609 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5610 name,
5611 kind: crate::ast::TypeKind::Enum { labels },
5612 }))
5613 }
5614
5615 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5616 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5617 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5618 /// consumed.
5619 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5620 let if_not_exists = self.parse_if_not_exists();
5621 let name = self.expect_ident_like()?;
5622 let mut columns: Vec<String> = Vec::new();
5623 if matches!(self.peek(), Token::LParen) {
5624 self.advance();
5625 loop {
5626 let c = self.expect_ident_like()?;
5627 columns.push(c);
5628 if matches!(self.peek(), Token::Comma) {
5629 self.advance();
5630 continue;
5631 }
5632 if matches!(self.peek(), Token::RParen) {
5633 self.advance();
5634 break;
5635 }
5636 return Err(self.err(alloc::format!(
5637 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5638 self.peek()
5639 )));
5640 }
5641 }
5642 if !matches!(self.peek(), Token::As) {
5643 return Err(self.err(alloc::format!(
5644 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5645 self.peek()
5646 )));
5647 }
5648 self.advance();
5649 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5650 // CTEs only; the engine rejects data-modifying ones with PG's
5651 // message). A trailing `WITH [NO] DATA` can't START the body,
5652 // so WITH here heads the query.
5653 let body = if self.peek_is_with_kw() {
5654 self.advance();
5655 self.parse_nested_with_select()?
5656 } else {
5657 let body_stmt = self.parse_select_stmt()?;
5658 let Statement::Select(body) = body_stmt else {
5659 return Err(self.err(alloc::format!(
5660 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5661 )));
5662 };
5663 body
5664 };
5665 // Optional trailing `WITH [NO] DATA`.
5666 let with_data = self.parse_optional_with_data(true)?;
5667 Ok(Statement::CreateMaterializedView(
5668 crate::ast::CreateMaterializedViewStatement {
5669 temporary: false,
5670 name,
5671 if_not_exists,
5672 columns,
5673 body,
5674 with_data,
5675 as_plain_table: false,
5676 },
5677 ))
5678 }
5679
5680 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5681 /// `default_when_absent` is what to return if the tail is
5682 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5683 /// WITH DATA).
5684 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5685 let save = self.pos;
5686 // `WITH` is an Ident (not reserved in the lexer).
5687 let is_with = match self.peek() {
5688 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5689 _ => false,
5690 };
5691 if !is_with {
5692 return Ok(default_when_absent);
5693 }
5694 self.advance();
5695 // Optional `NO`.
5696 let mut with_data = true;
5697 let is_no = match self.peek() {
5698 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5699 _ => false,
5700 };
5701 if is_no {
5702 self.advance();
5703 with_data = false;
5704 }
5705 // Required `DATA` ident.
5706 let is_data = match self.peek() {
5707 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5708 _ => false,
5709 };
5710 if is_data {
5711 self.advance();
5712 Ok(with_data)
5713 } else {
5714 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5715 // parser can interpret it.
5716 self.pos = save;
5717 Ok(default_when_absent)
5718 }
5719 }
5720
5721 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5722 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5723 /// All keyword prefixes have already been consumed; the flags
5724 /// say which were present.
5725 fn parse_create_view_after_keyword(
5726 &mut self,
5727 or_replace: bool,
5728 _materialized_unused: bool,
5729 temporary: bool,
5730 ) -> Result<Statement, ParseError> {
5731 let if_not_exists = self.parse_if_not_exists();
5732 let name = self.expect_ident_like()?;
5733 // Optional `(col, col, …)` rename list.
5734 let mut columns: Vec<String> = Vec::new();
5735 if matches!(self.peek(), Token::LParen) {
5736 self.advance();
5737 loop {
5738 let c = self.expect_ident_like()?;
5739 columns.push(c);
5740 if matches!(self.peek(), Token::Comma) {
5741 self.advance();
5742 continue;
5743 }
5744 if matches!(self.peek(), Token::RParen) {
5745 self.advance();
5746 break;
5747 }
5748 return Err(self.err(alloc::format!(
5749 "expected , or ) in VIEW column list, got {:?}",
5750 self.peek()
5751 )));
5752 }
5753 }
5754 // Required `AS`.
5755 if !matches!(self.peek(), Token::As) {
5756 return Err(self.err(alloc::format!(
5757 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5758 self.peek()
5759 )));
5760 }
5761 self.advance();
5762 // Body: a regular SELECT statement. v7.39 (round 151) — a
5763 // WITH-headed body is legal too (read-only CTEs only; the
5764 // engine rejects data-modifying ones with PG's message).
5765 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5766 // with the check-option clause, so WITH here heads the query.
5767 let body = if self.peek_is_with_kw() {
5768 self.advance();
5769 self.parse_nested_with_select()?
5770 } else {
5771 let body_stmt = self.parse_select_stmt()?;
5772 let Statement::Select(body) = body_stmt else {
5773 return Err(self.err(alloc::format!(
5774 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5775 )));
5776 };
5777 body
5778 };
5779 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5780 // The SELECT parser stops before a trailing WITH, so it lands here.
5781 let check_option = if matches!(self.peek(),
5782 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5783 {
5784 self.advance(); // WITH
5785 let opt = match self.peek() {
5786 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5787 self.advance();
5788 crate::ast::ViewCheckOption::Local
5789 }
5790 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5791 self.advance();
5792 crate::ast::ViewCheckOption::Cascaded
5793 }
5794 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5795 _ => crate::ast::ViewCheckOption::Cascaded,
5796 };
5797 if !matches!(self.peek(),
5798 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5799 {
5800 return Err(self.err(alloc::format!(
5801 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5802 self.peek()
5803 )));
5804 }
5805 self.advance(); // CHECK
5806 if !matches!(self.peek(),
5807 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5808 {
5809 return Err(self.err(alloc::format!(
5810 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5811 self.peek()
5812 )));
5813 }
5814 self.advance(); // OPTION
5815 Some(opt)
5816 } else {
5817 None
5818 };
5819 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5820 name,
5821 or_replace,
5822 if_not_exists,
5823 temporary,
5824 columns,
5825 body,
5826 check_option,
5827 }))
5828 }
5829
5830 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5831 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5832 /// consumed; `temporary` carries whether TEMPORARY was seen.
5833 fn parse_create_sequence_after_keyword(
5834 &mut self,
5835 temporary: bool,
5836 ) -> Result<Statement, ParseError> {
5837 let if_not_exists = self.parse_if_not_exists();
5838 let name = self.expect_ident_like()?;
5839 // Optional `AS data_type`.
5840 let data_type = if matches!(self.peek(), Token::As) {
5841 self.advance();
5842 Some(self.parse_sequence_data_type()?)
5843 } else {
5844 None
5845 };
5846 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5847 Ok(Statement::CreateSequence(
5848 crate::ast::CreateSequenceStatement {
5849 name,
5850 if_not_exists,
5851 temporary,
5852 data_type,
5853 options,
5854 },
5855 ))
5856 }
5857
5858 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5859 /// already been consumed; this is reached after `SEQUENCE`.
5860 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5861 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5862 use crate::ast::AlterDomainAction as A;
5863 let name = self.expect_ident_like()?;
5864 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5865 let kw = match self.peek() {
5866 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5867 Token::Drop => alloc::string::String::from("drop"),
5868 Token::Default => alloc::string::String::from("default"),
5869 other => {
5870 return Err(self.err(alloc::format!(
5871 "expected an ALTER DOMAIN action, got {other:?}"
5872 )));
5873 }
5874 };
5875 let action = match kw.as_str() {
5876 "add" => {
5877 self.advance();
5878 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5879 {
5880 self.advance();
5881 Some(self.expect_ident_like()?)
5882 } else {
5883 None
5884 };
5885 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5886 return Err(self.err(alloc::format!(
5887 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5888 self.peek()
5889 )));
5890 }
5891 self.advance();
5892 if !matches!(self.peek(), Token::LParen) {
5893 return Err(self.err("expected '(' after CHECK".into()));
5894 }
5895 self.advance();
5896 let check = self.parse_expr(0)?;
5897 if !matches!(self.peek(), Token::RParen) {
5898 return Err(self.err("expected ')' after CHECK expression".into()));
5899 }
5900 self.advance();
5901 A::AddConstraint { name: cname, check }
5902 }
5903 "drop" => {
5904 self.advance();
5905 match self.peek() {
5906 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5907 self.advance();
5908 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5909 {
5910 self.advance();
5911 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5912 {
5913 return Err(self.err("expected EXISTS after IF".into()));
5914 }
5915 self.advance();
5916 true
5917 } else {
5918 false
5919 };
5920 let cn = self.expect_ident_like()?;
5921 A::DropConstraint {
5922 name: cn,
5923 if_exists,
5924 }
5925 }
5926 Token::Default => {
5927 self.advance();
5928 A::DropDefault
5929 }
5930 Token::Not => {
5931 self.advance();
5932 if !matches!(self.peek(), Token::Null) {
5933 return Err(self.err("expected NULL after NOT".into()));
5934 }
5935 self.advance();
5936 A::DropNotNull
5937 }
5938 other => {
5939 return Err(self.err(alloc::format!(
5940 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5941 )));
5942 }
5943 }
5944 }
5945 "set" => {
5946 self.advance();
5947 match self.peek() {
5948 Token::Default => {
5949 self.advance();
5950 A::SetDefault(self.parse_expr(0)?)
5951 }
5952 Token::Not => {
5953 self.advance();
5954 if !matches!(self.peek(), Token::Null) {
5955 return Err(self.err("expected NULL after NOT".into()));
5956 }
5957 self.advance();
5958 A::SetNotNull
5959 }
5960 other => {
5961 return Err(self.err(alloc::format!(
5962 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5963 )));
5964 }
5965 }
5966 }
5967 "rename" => {
5968 self.advance();
5969 if !matches!(self.peek(), Token::To) {
5970 return Err(self.err("expected TO after RENAME".into()));
5971 }
5972 self.advance();
5973 A::RenameTo(self.expect_ident_like()?)
5974 }
5975 other => {
5976 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5977 }
5978 };
5979 Ok(Statement::AlterDomain { name, action })
5980 }
5981
5982 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5983 let if_exists = self.parse_if_exists();
5984 let name = self.expect_ident_like()?;
5985 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5986 // the option list (PG allows only one or the other).
5987 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
5988 self.advance();
5989 if matches!(self.peek(), Token::To) {
5990 self.advance();
5991 } else {
5992 self.expect_keyword_ident("to")?;
5993 }
5994 let new = self.expect_ident_like()?;
5995 return Ok(Statement::AlterSequence(
5996 crate::ast::AlterSequenceStatement {
5997 name,
5998 if_exists,
5999 options: crate::ast::SequenceOptions::default(),
6000 rename_to: Some(new),
6001 },
6002 ));
6003 }
6004 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6005 Ok(Statement::AlterSequence(
6006 crate::ast::AlterSequenceStatement {
6007 name,
6008 if_exists,
6009 options,
6010 rename_to: None,
6011 },
6012 ))
6013 }
6014
6015 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6016 let kw = self.expect_ident_like()?;
6017 match kw.to_ascii_lowercase().as_str() {
6018 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6019 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6020 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6021 other => Err(self.err(alloc::format!(
6022 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6023 ))),
6024 }
6025 }
6026
6027 fn parse_sequence_options(
6028 &mut self,
6029 allow_restart: bool,
6030 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6031 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6032 let mut opts = SequenceOptions::default();
6033 #[allow(clippy::while_let_loop)]
6034 loop {
6035 // Match an ident; stop at any non-ident token (sentinel,
6036 // semicolon, end of statement).
6037 let kw_lc = match self.peek() {
6038 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6039 _ => break,
6040 };
6041 match kw_lc.as_str() {
6042 "increment" => {
6043 self.advance();
6044 // Optional BY.
6045 if self.peek_is_by() {
6046 self.advance();
6047 }
6048 opts.increment = Some(self.expect_signed_int()?);
6049 }
6050 "minvalue" => {
6051 self.advance();
6052 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6053 }
6054 "maxvalue" => {
6055 self.advance();
6056 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6057 }
6058 "no" => {
6059 self.advance();
6060 let what = self.expect_ident_like()?;
6061 match what.to_ascii_lowercase().as_str() {
6062 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6063 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6064 "cycle" => opts.cycle = Some(false),
6065 other => {
6066 return Err(self.err(alloc::format!(
6067 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6068 )));
6069 }
6070 }
6071 }
6072 "start" => {
6073 self.advance();
6074 // Optional WITH.
6075 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6076 if s.eq_ignore_ascii_case("with"))
6077 {
6078 self.advance();
6079 }
6080 opts.start = Some(self.expect_signed_int()?);
6081 }
6082 "restart" if allow_restart => {
6083 self.advance();
6084 // Optional WITH n; bare RESTART means restart at START.
6085 let mut with_val: Option<i64> = None;
6086 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6087 if s.eq_ignore_ascii_case("with"))
6088 {
6089 self.advance();
6090 with_val = Some(self.expect_signed_int()?);
6091 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6092 with_val = Some(self.expect_signed_int()?);
6093 }
6094 opts.restart = Some(with_val);
6095 }
6096 "cache" => {
6097 self.advance();
6098 opts.cache = Some(self.expect_signed_int()?);
6099 }
6100 "cycle" => {
6101 self.advance();
6102 opts.cycle = Some(true);
6103 }
6104 "owned" => {
6105 self.advance();
6106 match self.peek() {
6107 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6108 self.advance();
6109 }
6110 other => {
6111 return Err(
6112 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6113 );
6114 }
6115 }
6116 // OWNED BY {NONE | tab.col}. Read just one ident
6117 // (NOT expect_ident_like which would auto-strip
6118 // a schema prefix and consume the `.col` we need).
6119 let first = match self.advance() {
6120 Token::Ident(s) | Token::QuotedIdent(s) => s,
6121 other => {
6122 return Err(self.err(alloc::format!(
6123 "expected identifier or NONE after OWNED BY, got {other:?}"
6124 )));
6125 }
6126 };
6127 if first.eq_ignore_ascii_case("none") {
6128 opts.owned_by = Some(SequenceOwnedBy::None);
6129 } else if matches!(self.peek(), Token::Dot) {
6130 self.advance();
6131 let second = match self.advance() {
6132 Token::Ident(s) | Token::QuotedIdent(s) => s,
6133 other => {
6134 return Err(self.err(alloc::format!(
6135 "expected column name after OWNED BY {first}., got {other:?}"
6136 )));
6137 }
6138 };
6139 // v7.17 dump-compat fix — pg_dump emits
6140 // OWNED BY clauses as
6141 // `schema.table.column` (three segments).
6142 // If a third `.<ident>` follows, treat the
6143 // first ident as schema (drop it; SPG is
6144 // single-schema) and the middle / last
6145 // pair as table.column. Otherwise it's
6146 // the two-segment form table.column.
6147 if matches!(self.peek(), Token::Dot) {
6148 self.advance();
6149 let third = match self.advance() {
6150 Token::Ident(s) | Token::QuotedIdent(s) => s,
6151 other => {
6152 return Err(self.err(alloc::format!(
6153 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6154 )));
6155 }
6156 };
6157 let _ = first; // schema prefix discarded
6158 opts.owned_by = Some(SequenceOwnedBy::Column {
6159 table: second,
6160 column: third,
6161 });
6162 } else {
6163 opts.owned_by = Some(SequenceOwnedBy::Column {
6164 table: first,
6165 column: second,
6166 });
6167 }
6168 } else {
6169 return Err(self.err(alloc::format!(
6170 "expected table.column or NONE after OWNED BY, got {first:?}"
6171 )));
6172 }
6173 }
6174 _ => break,
6175 }
6176 }
6177 Ok(opts)
6178 }
6179
6180 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6181 let neg = if matches!(self.peek(), Token::Minus) {
6182 self.advance();
6183 true
6184 } else {
6185 false
6186 };
6187 match self.peek() {
6188 Token::Integer(n) => {
6189 let v = *n;
6190 self.advance();
6191 Ok(if neg { -v } else { v })
6192 }
6193 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6194 }
6195 }
6196
6197 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6198 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6199 /// clause is fully accepted and discarded — SPG always runs
6200 /// constraint checks immediately (single-writer model). The
6201 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6202 /// in either order (per the SQL spec they're independent),
6203 /// though pg_dump always emits them in the canonical
6204 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6205 /// Stops at the first token that isn't part of the clause.
6206 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6207 self.consume_deferrable_clauses_timed().map(|_| ())
6208 }
6209
6210 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6211 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6212 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6213 /// NOT DEFERRABLE and a circular-FK migration could not load.
6214 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6215 let mut deferrable = false;
6216 let mut initially_deferred = false;
6217 loop {
6218 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6219 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6220 self.advance();
6221 deferrable = true;
6222 if self.consume_optional_initially_clause()? {
6223 initially_deferred = true;
6224 }
6225 continue;
6226 }
6227 // `NOT DEFERRABLE` — already worked pre-3.1.
6228 if matches!(self.peek(), Token::Not) {
6229 let look = self.tokens.get(self.pos + 1);
6230 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6231 self.advance(); // NOT
6232 self.advance(); // DEFERRABLE
6233 deferrable = false;
6234 initially_deferred = false;
6235 let _ = self.consume_optional_initially_clause()?;
6236 continue;
6237 }
6238 break;
6239 }
6240 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6241 // accepts this without a leading [NOT] DEFERRABLE
6242 // (the timing keyword alone). pg_dump occasionally
6243 // emits it on FK constraints that inherit timing.
6244 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6245 if self.consume_optional_initially_clause()? {
6246 initially_deferred = true;
6247 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6248 deferrable = true;
6249 }
6250 continue;
6251 }
6252 break;
6253 }
6254 Ok((deferrable, initially_deferred))
6255 }
6256
6257 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6258 /// next token is `INITIALLY`, consume it plus the required
6259 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6260 /// Returns true when the timing seen was `DEFERRED`.
6261 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6262 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6263 return Ok(false);
6264 }
6265 self.advance(); // INITIALLY
6266 match self.advance() {
6267 Token::Ident(s)
6268 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6269 {
6270 Ok(s.eq_ignore_ascii_case("deferred"))
6271 }
6272 other => Err(self.err(alloc::format!(
6273 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6274 ))),
6275 }
6276 }
6277
6278 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6279 /// in its entirety so the parser returns Empty without
6280 /// touching the runtime. The CREATE+PROCEDURE keywords are
6281 /// already consumed; this swallows everything from the
6282 /// procedure name through the matching `END`, including
6283 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6284 /// (DELIMITER `//` makes the script splitter forward the
6285 /// whole block as one statement), `@var` session-variable
6286 /// references, and the trailing terminator.
6287 ///
6288 /// Tracks nesting depth so:
6289 /// BEGIN
6290 /// IF cond THEN
6291 /// BEGIN ... END;
6292 /// END IF;
6293 /// END
6294 /// terminates at the outer END.
6295 fn consume_mysql_routine_body(&mut self) {
6296 // Outer skeleton: name, (...), optional clauses, BEGIN
6297 // <body> END [;]. Scan for the first BEGIN — anything
6298 // before it is signature decoration we don't care about.
6299 // Once inside BEGIN, count up on BEGIN, down on END.
6300 let mut depth: i32 = 0;
6301 let mut started = false;
6302 loop {
6303 match self.peek().clone() {
6304 Token::Begin => {
6305 self.advance();
6306 depth += 1;
6307 started = true;
6308 }
6309 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6310 self.advance();
6311 if started {
6312 depth -= 1;
6313 if depth <= 0 {
6314 // Optional trailing ident (`END IF`,
6315 // `END LOOP`, `END WHILE`, `END CASE`,
6316 // `END label_name`) — eat the next
6317 // ident if present so we don't
6318 // mistake `END IF;` for the outer
6319 // close.
6320 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6321 // If the next token is one of the
6322 // PL/SQL block-closer keywords,
6323 // the END belongs to an inner
6324 // block; bump depth back up.
6325 let is_inner_close = matches!(
6326 self.peek(),
6327 Token::Ident(s) | Token::QuotedIdent(s)
6328 if matches!(
6329 s.to_ascii_lowercase().as_str(),
6330 "if" | "loop" | "while" | "case" | "repeat"
6331 )
6332 );
6333 if is_inner_close {
6334 self.advance();
6335 depth += 1;
6336 continue;
6337 }
6338 }
6339 // Eat optional trailing `;`.
6340 if matches!(self.peek(), Token::Semicolon) {
6341 self.advance();
6342 }
6343 return;
6344 }
6345 }
6346 }
6347 Token::Eof => return,
6348 _ => {
6349 self.advance();
6350 }
6351 }
6352 }
6353 }
6354
6355 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6356 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6357 ///
6358 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6359 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6360 /// ident, or `ident @ ident-or-quoted-string` host form)
6361 /// * `SQL SECURITY {DEFINER|INVOKER}`
6362 ///
6363 /// Each clause may appear at most once but in any order.
6364 /// The hints are pure planner / permission metadata that
6365 /// SPG's view-rewrite engine handles uniformly; we accept
6366 /// and discard. Returns `Ok(())` once a non-clause token is
6367 /// peeked (the caller then checks for the `VIEW` keyword).
6368 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6369 loop {
6370 match self.peek().clone() {
6371 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6372 self.advance(); // ALGORITHM
6373 // Optional `=`. MySQL spec requires it but be
6374 // generous.
6375 if matches!(self.peek(), Token::Eq) {
6376 self.advance();
6377 }
6378 // UNDEFINED / MERGE / TEMPTABLE — accept any
6379 // bare ident; unknown values still parse so
6380 // future MySQL versions don't break.
6381 if matches!(
6382 self.peek(),
6383 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6384 ) {
6385 self.advance();
6386 }
6387 }
6388 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6389 self.advance(); // DEFINER
6390 if matches!(self.peek(), Token::Eq) {
6391 self.advance();
6392 }
6393 // User: quoted string, ident, OR ident @ host
6394 // (host may itself be quoted or bare).
6395 match self.peek().clone() {
6396 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6397 self.advance();
6398 // Optional `@host`.
6399 if matches!(self.peek(), Token::At) {
6400 self.advance();
6401 if matches!(
6402 self.peek(),
6403 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6404 ) {
6405 self.advance();
6406 }
6407 }
6408 }
6409 _ => {}
6410 }
6411 }
6412 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6413 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6414 // when followed by SECURITY — the dispatcher must
6415 // not consume a bare `SQL` token (it's not a
6416 // legal CREATE prefix on its own).
6417 let save = self.pos;
6418 self.advance(); // SQL
6419 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6420 if s2.eq_ignore_ascii_case("security"))
6421 {
6422 self.advance(); // SECURITY
6423 // DEFINER / INVOKER trailing ident.
6424 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6425 self.advance();
6426 }
6427 } else {
6428 // Not a SQL SECURITY clause — roll back and
6429 // bail; the caller will error out cleanly.
6430 self.pos = save;
6431 return Ok(());
6432 }
6433 }
6434 _ => return Ok(()),
6435 }
6436 }
6437 }
6438
6439 fn parse_if_not_exists(&mut self) -> bool {
6440 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6441 {
6442 let save = self.pos;
6443 self.advance();
6444 if matches!(self.peek(), Token::Not) {
6445 self.advance();
6446 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6447 {
6448 self.advance();
6449 return true;
6450 }
6451 }
6452 self.pos = save;
6453 }
6454 false
6455 }
6456
6457 fn parse_if_exists(&mut self) -> bool {
6458 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6459 {
6460 let save = self.pos;
6461 self.advance();
6462 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6463 {
6464 self.advance();
6465 return true;
6466 }
6467 self.pos = save;
6468 }
6469 false
6470 }
6471
6472 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6473 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6474 /// been consumed.
6475 fn parse_create_trigger_after_keyword(
6476 &mut self,
6477 or_replace: bool,
6478 ) -> Result<Statement, ParseError> {
6479 let name = self.expect_ident_like()?;
6480 let timing = {
6481 let ident = self.expect_ident_like()?;
6482 if ident.eq_ignore_ascii_case("before") {
6483 TriggerTiming::Before
6484 } else if ident.eq_ignore_ascii_case("after") {
6485 TriggerTiming::After
6486 } else if ident.eq_ignore_ascii_case("instead") {
6487 let next = self.expect_ident_like()?;
6488 if !next.eq_ignore_ascii_case("of") {
6489 return Err(self.err(alloc::format!(
6490 "expected OF after INSTEAD in trigger timing, got {next:?}"
6491 )));
6492 }
6493 TriggerTiming::InsteadOf
6494 } else {
6495 return Err(self.err(alloc::format!(
6496 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6497 )));
6498 }
6499 };
6500 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6501 // OR is a reserved keyword token (Token::Or), not an Ident.
6502 // v7.13.0 — after an UPDATE event we may optionally see
6503 // `OF col, col, …` (mailrs round-5 G7). Columns are
6504 // captured into `update_columns` once across the whole
6505 // events list; multiple `UPDATE OF` clauses are rejected.
6506 let mut events: Vec<TriggerEvent> = Vec::new();
6507 let mut update_columns: Vec<String> = Vec::new();
6508 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6509 events.push(first_ev);
6510 if !first_cols.is_empty() {
6511 update_columns = first_cols;
6512 }
6513 while matches!(self.peek(), Token::Or) {
6514 self.advance();
6515 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6516 events.push(ev);
6517 if !cols.is_empty() {
6518 if !update_columns.is_empty() {
6519 return Err(
6520 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6521 );
6522 }
6523 update_columns = cols;
6524 }
6525 }
6526 // ON <table>
6527 let tok = self.peek();
6528 let Token::On = tok else {
6529 return Err(self.err(alloc::format!(
6530 "expected ON after trigger events, got {tok:?}"
6531 )));
6532 };
6533 self.advance();
6534 let table = self.expect_ident_like()?;
6535 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6536 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6537 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6538 // the trigger as a plain AFTER trigger (correct for every non-deferred
6539 // use; deferral timing is not yet honoured).
6540 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6541 if s.eq_ignore_ascii_case("from"))
6542 {
6543 self.advance();
6544 let _reftable = self.expect_ident_like()?;
6545 }
6546 self.consume_optional_deferrable_clauses()?;
6547 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6548 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6549 // idents.
6550 if !matches!(self.peek(), Token::For) {
6551 return Err(self.err(alloc::format!(
6552 "expected FOR EACH ROW / STATEMENT, got {:?}",
6553 self.peek()
6554 )));
6555 }
6556 self.advance();
6557 let for_each = {
6558 let e = self.expect_ident_like()?;
6559 if !e.eq_ignore_ascii_case("each") {
6560 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6561 }
6562 let unit = self.expect_ident_like()?;
6563 if unit.eq_ignore_ascii_case("row") {
6564 TriggerForEach::Row
6565 } else if unit.eq_ignore_ascii_case("statement") {
6566 TriggerForEach::Statement
6567 } else {
6568 return Err(self.err(alloc::format!(
6569 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6570 )));
6571 }
6572 };
6573 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6574 let when_condition = if matches!(self.peek(),
6575 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6576 {
6577 self.advance();
6578 Some(self.parse_paren_expr("WHEN")?)
6579 } else {
6580 None
6581 };
6582 // EXECUTE FUNCTION/PROCEDURE name(...)
6583 let exec = self.expect_ident_like()?;
6584 if !exec.eq_ignore_ascii_case("execute") {
6585 return Err(self.err(alloc::format!(
6586 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6587 )));
6588 }
6589 let fn_or_proc = self.expect_ident_like()?;
6590 if !(fn_or_proc.eq_ignore_ascii_case("function")
6591 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6592 {
6593 return Err(self.err(alloc::format!(
6594 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6595 )));
6596 }
6597 let function = self.expect_ident_like()?;
6598 // Optional empty arg list `()`.
6599 if matches!(self.peek(), Token::LParen) {
6600 self.advance();
6601 if !matches!(self.peek(), Token::RParen) {
6602 return Err(self.err(alloc::format!(
6603 "v7.12.4 trigger function calls take no args; got {:?}",
6604 self.peek()
6605 )));
6606 }
6607 self.advance();
6608 }
6609 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6610 name,
6611 or_replace,
6612 timing,
6613 events,
6614 table,
6615 for_each,
6616 function,
6617 update_columns,
6618 when_condition,
6619 }))
6620 }
6621
6622 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6623 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6624 fn parse_create_rule_after_keyword(
6625 &mut self,
6626 or_replace: bool,
6627 ) -> Result<Statement, ParseError> {
6628 let name = self.expect_ident_like()?;
6629 if !matches!(self.peek(), Token::As) {
6630 return Err(self.err(alloc::format!(
6631 "expected AS in CREATE RULE, got {:?}",
6632 self.peek()
6633 )));
6634 }
6635 self.advance();
6636 if !matches!(self.peek(), Token::On) {
6637 return Err(self.err(alloc::format!(
6638 "expected ON in CREATE RULE, got {:?}",
6639 self.peek()
6640 )));
6641 }
6642 self.advance();
6643 let event = self.parse_rule_event()?;
6644 if !matches!(self.peek(), Token::To)
6645 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6646 {
6647 return Err(self.err(alloc::format!(
6648 "expected TO after rule event, got {:?}",
6649 self.peek()
6650 )));
6651 }
6652 self.advance();
6653 let table = self.expect_ident_like()?;
6654 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6655 let when_condition = if matches!(self.peek(), Token::Where) {
6656 self.advance();
6657 Some(self.parse_expr(0)?)
6658 } else {
6659 None
6660 };
6661 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6662 {
6663 return Err(self.err(alloc::format!(
6664 "expected DO in CREATE RULE, got {:?}",
6665 self.peek()
6666 )));
6667 }
6668 self.advance();
6669 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6670 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6671 {
6672 self.advance();
6673 true
6674 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6675 self.advance();
6676 false
6677 } else {
6678 false
6679 };
6680 // `NOTHING` | `( cmd; … )` | `cmd`.
6681 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6682 {
6683 self.advance();
6684 Vec::new()
6685 } else if matches!(self.peek(), Token::LParen) {
6686 self.advance();
6687 let mut cmds = Vec::new();
6688 loop {
6689 cmds.push(self.parse_one_statement()?);
6690 if matches!(self.peek(), Token::Semicolon) {
6691 self.advance();
6692 if matches!(self.peek(), Token::RParen) {
6693 break;
6694 }
6695 continue;
6696 }
6697 break;
6698 }
6699 if !matches!(self.peek(), Token::RParen) {
6700 return Err(self.err(alloc::format!(
6701 "expected ) closing the CREATE RULE command list, got {:?}",
6702 self.peek()
6703 )));
6704 }
6705 self.advance();
6706 cmds
6707 } else {
6708 alloc::vec![self.parse_one_statement()?]
6709 };
6710 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6711 name,
6712 or_replace,
6713 event,
6714 table,
6715 instead,
6716 when_condition,
6717 commands,
6718 }))
6719 }
6720
6721 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6722 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6723 if matches!(self.peek(), Token::Insert) {
6724 self.advance();
6725 return Ok(alloc::string::String::from("INSERT"));
6726 }
6727 if matches!(self.peek(), Token::Select) {
6728 self.advance();
6729 return Ok(alloc::string::String::from("SELECT"));
6730 }
6731 match self.peek() {
6732 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6733 self.advance();
6734 Ok(alloc::string::String::from("UPDATE"))
6735 }
6736 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6737 self.advance();
6738 Ok(alloc::string::String::from("DELETE"))
6739 }
6740 other => Err(self.err(alloc::format!(
6741 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6742 ))),
6743 }
6744 }
6745
6746 /// v7.13.0 — parse one trigger event, then optionally consume
6747 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6748 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6749 fn parse_trigger_event_with_optional_of(
6750 &mut self,
6751 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6752 let ev = self.parse_trigger_event()?;
6753 if !matches!(ev, TriggerEvent::Update) {
6754 return Ok((ev, Vec::new()));
6755 }
6756 // `OF` is a bare ident.
6757 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6758 return Ok((ev, Vec::new()));
6759 }
6760 self.advance(); // OF
6761 let mut cols: Vec<String> = Vec::new();
6762 loop {
6763 cols.push(self.expect_ident_like()?);
6764 if matches!(self.peek(), Token::Comma) {
6765 self.advance();
6766 continue;
6767 }
6768 break;
6769 }
6770 if cols.is_empty() {
6771 return Err(
6772 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6773 );
6774 }
6775 Ok((ev, cols))
6776 }
6777
6778 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6779 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6780 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6781 /// inside the body.
6782 /// Called by [`parse_plpgsql_body`] after the body's tokens
6783 /// have been lexed into this temporary parser.
6784 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6785 // v7.12.6 — optional DECLARE prelude.
6786 let declarations = if matches!(
6787 self.peek(),
6788 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6789 ) {
6790 self.advance();
6791 self.parse_plpgsql_declare_block()?
6792 } else {
6793 Vec::new()
6794 };
6795 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6796 // `BEGIN` transaction-start, but we can reuse the
6797 // reserved Token::Begin since the body is a separate
6798 // lex/parse context).
6799 if !matches!(self.peek(), Token::Begin) {
6800 return Err(self.err(alloc::format!(
6801 "expected BEGIN at start of plpgsql block, got {:?}",
6802 self.peek()
6803 )));
6804 }
6805 self.advance();
6806 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6807 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6808 // body's last statement and the trailing END. When present
6809 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6810 // arms terminated by END.
6811 let exception_handlers = if matches!(
6812 self.peek(),
6813 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6814 ) {
6815 self.advance();
6816 self.parse_plpgsql_exception_handlers()?
6817 } else {
6818 Vec::new()
6819 };
6820 Ok(PlPgSqlBlock {
6821 declarations,
6822 statements,
6823 exception_handlers,
6824 })
6825 }
6826
6827 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6828 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6829 fn parse_plpgsql_exception_handlers(
6830 &mut self,
6831 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6832 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6833 loop {
6834 // Stop at END — the block-level trailing END LOOP / END;
6835 // is handled by the caller.
6836 if matches!(
6837 self.peek(),
6838 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6839 ) {
6840 return Ok(out);
6841 }
6842 // WHEN <cond> [OR <cond>]* THEN <body>
6843 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6844 {
6845 return Err(self.err(alloc::format!(
6846 "expected WHEN or END inside EXCEPTION clause, got {:?}",
6847 self.peek()
6848 )));
6849 }
6850 self.advance();
6851 let mut conditions: Vec<String> = Vec::new();
6852 conditions.push(self.expect_ident_like()?);
6853 while matches!(self.peek(), Token::Or) {
6854 self.advance();
6855 conditions.push(self.expect_ident_like()?);
6856 }
6857 let then_kw = self.expect_ident_like()?;
6858 if !then_kw.eq_ignore_ascii_case("then") {
6859 return Err(self.err(alloc::format!(
6860 "expected THEN after WHEN condition list, got {then_kw:?}"
6861 )));
6862 }
6863 let body = self.parse_plpgsql_stmt_list_until_end()?;
6864 out.push(crate::ast::ExceptionHandler { conditions, body });
6865 }
6866 }
6867
6868 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6869 /// prelude. Caller has already consumed `DECLARE`. We stop
6870 /// reading entries when we hit `BEGIN`.
6871 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6872 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6873 loop {
6874 if matches!(self.peek(), Token::Begin) {
6875 return Ok(out);
6876 }
6877 let name = self.expect_ident_like()?;
6878 // v7.37.20 (20.7) — type inference: if the next token is
6879 // `:=` or `=` (no explicit type), infer from the default
6880 // expression. Otherwise the ident that follows is the
6881 // declared type.
6882 //
6883 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6884 // (PG-standard). SPG parse-accepts and treats identically
6885 // to inference — the eventual runtime value determines
6886 // the local's type, which is faithful to how SPG handles
6887 // untyped locals today (see 20.7). Full compile-time
6888 // catalog lookup queues with v7.40 PL/pgSQL epic.
6889 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6890 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6891 // downstream declaration walker to type the local by
6892 // the runtime type of the default expression.
6893 FunctionArgType::Raw("_infer_".into())
6894 } else {
6895 let ty_token = self.expect_ident_like()?;
6896 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6897 // consume optional `.<ident>` qualifier + `%<KW>`
6898 // suffix. Both qualifier and suffix map to _infer_.
6899 if matches!(self.peek(), Token::Dot) {
6900 self.advance();
6901 let _ = self.expect_ident_like()?;
6902 }
6903 if matches!(self.peek(), Token::Percent) {
6904 self.advance();
6905 // Consume the trailing TYPE / ROWTYPE ident.
6906 let _ = self.expect_ident_like()?;
6907 FunctionArgType::Raw("_infer_".into())
6908 } else {
6909 match map_type_ident_to_column_type_name(&ty_token) {
6910 Some(t) => FunctionArgType::Typed(t),
6911 None => FunctionArgType::Raw(ty_token),
6912 }
6913 }
6914 };
6915 let default = match self.peek() {
6916 Token::ColonEq => {
6917 self.advance();
6918 Some(self.parse_expr(0)?)
6919 }
6920 Token::Eq => {
6921 // PL/pgSQL also accepts `=` for the
6922 // DECLARE default (PG treats them the same
6923 // in this position).
6924 self.advance();
6925 Some(self.parse_expr(0)?)
6926 }
6927 _ => None,
6928 };
6929 // Mandatory `;` between declarations.
6930 if !matches!(self.peek(), Token::Semicolon) {
6931 return Err(self.err(alloc::format!(
6932 "expected ; after DECLARE entry for {name:?}, got {:?}",
6933 self.peek()
6934 )));
6935 }
6936 self.advance();
6937 out.push(PlPgSqlDeclare { name, ty, default });
6938 }
6939 }
6940
6941 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6942 /// the terminating `END;` (or `END IF;` etc — handled by the
6943 /// per-construct sub-parsers). Used by both the outer block
6944 /// and the IF/ELSE branch bodies.
6945 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6946 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6947 loop {
6948 // Allow trailing semicolons + END.
6949 while matches!(self.peek(), Token::Semicolon) {
6950 self.advance();
6951 }
6952 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6953 if matches!(
6954 self.peek(),
6955 Token::Ident(s) | Token::QuotedIdent(s)
6956 if s.eq_ignore_ascii_case("end")
6957 || s.eq_ignore_ascii_case("else")
6958 || s.eq_ignore_ascii_case("elsif")
6959 || s.eq_ignore_ascii_case("elseif")
6960 || s.eq_ignore_ascii_case("exception")
6961 || s.eq_ignore_ascii_case("when")
6962 ) {
6963 return Ok(statements);
6964 }
6965 // Otherwise: one statement, then expect `;` or
6966 // a block-terminator keyword.
6967 let stmt = self.parse_plpgsql_stmt()?;
6968 statements.push(stmt);
6969 match self.peek() {
6970 Token::Semicolon => {
6971 self.advance();
6972 }
6973 Token::Ident(s) | Token::QuotedIdent(s)
6974 if s.eq_ignore_ascii_case("end")
6975 || s.eq_ignore_ascii_case("else")
6976 || s.eq_ignore_ascii_case("elsif")
6977 || s.eq_ignore_ascii_case("elseif")
6978 || s.eq_ignore_ascii_case("exception")
6979 || s.eq_ignore_ascii_case("when") =>
6980 {
6981 // Final statement of the block without `;`.
6982 }
6983 other => {
6984 return Err(self.err(alloc::format!(
6985 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6986 )));
6987 }
6988 }
6989 }
6990 }
6991
6992 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
6993 // RETURN keyword?
6994 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
6995 {
6996 self.advance();
6997 return self.parse_plpgsql_return();
6998 }
6999 // v7.12.6 — IF block.
7000 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7001 {
7002 self.advance();
7003 return self.parse_plpgsql_if();
7004 }
7005 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7006 // Detected by peeking that token pos+3 is Ident("execute").
7007 if matches!(self.peek(), Token::For)
7008 && matches!(
7009 self.tokens.get(self.pos + 1),
7010 Some(Token::Ident(_) | Token::QuotedIdent(_))
7011 )
7012 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7013 && matches!(
7014 self.tokens.get(self.pos + 3),
7015 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7016 )
7017 {
7018 self.advance(); // FOR
7019 let var = self.expect_ident_like()?;
7020 self.advance(); // IN
7021 self.advance(); // EXECUTE
7022 // Prescan for LOOP at paren depth 0 so parse_expr stops
7023 // before the LOOP keyword (same trick as the bare-SELECT
7024 // ForQuery arm).
7025 let mut depth: i32 = 0;
7026 let mut loop_pos: Option<usize> = None;
7027 let mut scan = self.pos;
7028 while scan < self.tokens.len() {
7029 match self.tokens.get(scan) {
7030 Some(Token::LParen) => depth += 1,
7031 Some(Token::RParen) => depth -= 1,
7032 Some(Token::Ident(s) | Token::QuotedIdent(s))
7033 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7034 {
7035 loop_pos = Some(scan);
7036 break;
7037 }
7038 _ => {}
7039 }
7040 scan += 1;
7041 }
7042 let loop_pos = loop_pos.ok_or_else(|| {
7043 self.err(alloc::format!(
7044 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7045 ))
7046 })?;
7047 let saved_loop = self.tokens[loop_pos].clone();
7048 self.tokens[loop_pos] = Token::Semicolon;
7049 let expr_result = self.parse_expr(0);
7050 self.tokens[loop_pos] = saved_loop;
7051 let sql_expr = expr_result?;
7052 let loop_kw = self.expect_ident_like()?;
7053 if !loop_kw.eq_ignore_ascii_case("loop") {
7054 return Err(self.err(alloc::format!(
7055 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7056 )));
7057 }
7058 let body = self.parse_plpgsql_stmt_list_until_end()?;
7059 let end_kw = self.expect_ident_like()?;
7060 if !end_kw.eq_ignore_ascii_case("end") {
7061 return Err(self.err(alloc::format!(
7062 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7063 )));
7064 }
7065 let loop_kw2 = self.expect_ident_like()?;
7066 if !loop_kw2.eq_ignore_ascii_case("loop") {
7067 return Err(self.err(alloc::format!(
7068 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7069 )));
7070 }
7071 return Ok(PlPgSqlStmt::ForExecute {
7072 var,
7073 sql_expr,
7074 body,
7075 });
7076 }
7077 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7078 //
7079 // Two syntactic forms:
7080 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7081 // FOR var IN (SELECT ...) LOOP ...
7082 //
7083 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7084 // the trailing `LOOP` keyword as a table alias, we prescan
7085 // forward to find LOOP at paren depth 0, splice a fake
7086 // Semicolon at that position (so SELECT parses cleanly),
7087 // then re-splice LOOP back in.
7088 //
7089 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7090 // LOOP directly — no scan required.
7091 if matches!(self.peek(), Token::For)
7092 && matches!(
7093 self.tokens.get(self.pos + 1),
7094 Some(Token::Ident(_) | Token::QuotedIdent(_))
7095 )
7096 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7097 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7098 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7099 {
7100 self.advance(); // FOR
7101 let var = self.expect_ident_like()?;
7102 // IN
7103 self.advance();
7104 let query = if matches!(self.peek(), Token::LParen) {
7105 // Paren-wrapped SELECT.
7106 self.advance();
7107 let inner = self.parse_select_stmt()?;
7108 let Statement::Select(q) = inner else {
7109 return Err(self.err(alloc::format!(
7110 "expected SELECT inside (…), got {:?}",
7111 self.peek()
7112 )));
7113 };
7114 if !matches!(self.peek(), Token::RParen) {
7115 return Err(self.err(alloc::format!(
7116 "expected ')' after FOR-IN-SELECT body, got {:?}",
7117 self.peek()
7118 )));
7119 }
7120 self.advance();
7121 q
7122 } else {
7123 // Bare SELECT: prescan to find the LOOP boundary.
7124 let mut depth: i32 = 0;
7125 let mut loop_pos: Option<usize> = None;
7126 let mut scan = self.pos;
7127 while scan < self.tokens.len() {
7128 match self.tokens.get(scan) {
7129 Some(Token::LParen) => depth += 1,
7130 Some(Token::RParen) => depth -= 1,
7131 Some(Token::Ident(s) | Token::QuotedIdent(s))
7132 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7133 {
7134 loop_pos = Some(scan);
7135 break;
7136 }
7137 _ => {}
7138 }
7139 scan += 1;
7140 }
7141 let loop_pos = loop_pos.ok_or_else(|| {
7142 self.err(alloc::format!(
7143 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7144 ))
7145 })?;
7146 // Swap the LOOP token with a synthetic Semicolon so
7147 // parse_select_stmt stops there, then restore afterward.
7148 let saved_loop = self.tokens[loop_pos].clone();
7149 self.tokens[loop_pos] = Token::Semicolon;
7150 let parse_result = self.parse_select_stmt();
7151 self.tokens[loop_pos] = saved_loop;
7152 let inner = parse_result?;
7153 let Statement::Select(q) = inner else {
7154 return Err(self.err(alloc::format!(
7155 "expected SELECT after FOR <var> IN, got {:?}",
7156 self.peek()
7157 )));
7158 };
7159 q
7160 };
7161 let loop_kw = self.expect_ident_like()?;
7162 if !loop_kw.eq_ignore_ascii_case("loop") {
7163 return Err(self.err(alloc::format!(
7164 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7165 )));
7166 }
7167 let body = self.parse_plpgsql_stmt_list_until_end()?;
7168 let end_kw = self.expect_ident_like()?;
7169 if !end_kw.eq_ignore_ascii_case("end") {
7170 return Err(self.err(alloc::format!(
7171 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7172 )));
7173 }
7174 let loop_kw2 = self.expect_ident_like()?;
7175 if !loop_kw2.eq_ignore_ascii_case("loop") {
7176 return Err(self.err(alloc::format!(
7177 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7178 )));
7179 }
7180 return Ok(PlPgSqlStmt::ForQuery {
7181 var,
7182 query: Box::new(query),
7183 body,
7184 });
7185 }
7186 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7187 // FOR is a reserved keyword token (Token::For).
7188 if matches!(self.peek(), Token::For)
7189 && matches!(
7190 self.tokens.get(self.pos + 1),
7191 Some(Token::Ident(_) | Token::QuotedIdent(_))
7192 )
7193 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7194 {
7195 self.advance(); // FOR
7196 let var = self.expect_ident_like()?;
7197 if !matches!(self.peek(), Token::In) {
7198 return Err(self.err(alloc::format!(
7199 "expected IN after FOR <var>, got {:?}",
7200 self.peek()
7201 )));
7202 }
7203 self.advance();
7204 let reverse = matches!(
7205 self.peek(),
7206 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7207 );
7208 if reverse {
7209 self.advance();
7210 }
7211 let start = self.parse_expr(0)?;
7212 if !matches!(self.peek(), Token::DotDot) {
7213 return Err(self.err(alloc::format!(
7214 "expected '..' between FOR loop bounds, got {:?}",
7215 self.peek()
7216 )));
7217 }
7218 self.advance();
7219 let end = self.parse_expr(0)?;
7220 let loop_kw = self.expect_ident_like()?;
7221 if !loop_kw.eq_ignore_ascii_case("loop") {
7222 return Err(self.err(alloc::format!(
7223 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7224 )));
7225 }
7226 let body = self.parse_plpgsql_stmt_list_until_end()?;
7227 let end_kw = self.expect_ident_like()?;
7228 if !end_kw.eq_ignore_ascii_case("end") {
7229 return Err(self.err(alloc::format!(
7230 "expected END LOOP after FOR body, got {end_kw:?}"
7231 )));
7232 }
7233 let loop_kw2 = self.expect_ident_like()?;
7234 if !loop_kw2.eq_ignore_ascii_case("loop") {
7235 return Err(self.err(alloc::format!(
7236 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7237 )));
7238 }
7239 return Ok(PlPgSqlStmt::ForRange {
7240 var,
7241 start,
7242 end,
7243 reverse,
7244 body,
7245 });
7246 }
7247 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7248 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7249 {
7250 self.advance();
7251 let body = self.parse_plpgsql_stmt_list_until_end()?;
7252 let end_kw = self.expect_ident_like()?;
7253 if !end_kw.eq_ignore_ascii_case("end") {
7254 return Err(self.err(alloc::format!(
7255 "expected END LOOP after LOOP body, got {end_kw:?}"
7256 )));
7257 }
7258 let loop_kw = self.expect_ident_like()?;
7259 if !loop_kw.eq_ignore_ascii_case("loop") {
7260 return Err(self.err(alloc::format!(
7261 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7262 )));
7263 }
7264 return Ok(PlPgSqlStmt::Loop { body });
7265 }
7266 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7267 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7268 {
7269 self.advance();
7270 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7271 {
7272 self.advance();
7273 Some(self.parse_expr(0)?)
7274 } else {
7275 None
7276 };
7277 return Ok(PlPgSqlStmt::Exit { when });
7278 }
7279 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7280 // already-parsed Statement or a runtime-computed SQL string.
7281 // The disambiguator vs the extended-query-protocol `EXECUTE
7282 // <stmt_name>` (which is a top-level Statement, not a
7283 // plpgsql line) is that inside a DO block / trigger body the
7284 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7285 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7286 {
7287 self.advance();
7288 let sql = self.parse_expr(0)?;
7289 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7290 }
7291 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7292 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7293 {
7294 self.advance();
7295 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7296 {
7297 self.advance();
7298 Some(self.parse_expr(0)?)
7299 } else {
7300 None
7301 };
7302 return Ok(PlPgSqlStmt::Continue { when });
7303 }
7304 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7305 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7306 {
7307 self.advance();
7308 let condition = self.parse_expr(0)?;
7309 let loop_kw = self.expect_ident_like()?;
7310 if !loop_kw.eq_ignore_ascii_case("loop") {
7311 return Err(self.err(alloc::format!(
7312 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7313 )));
7314 }
7315 let body = self.parse_plpgsql_stmt_list_until_end()?;
7316 // Expect END LOOP.
7317 let end_kw = self.expect_ident_like()?;
7318 if !end_kw.eq_ignore_ascii_case("end") {
7319 return Err(self.err(alloc::format!(
7320 "expected END LOOP after WHILE body, got {end_kw:?}"
7321 )));
7322 }
7323 let loop_kw2 = self.expect_ident_like()?;
7324 if !loop_kw2.eq_ignore_ascii_case("loop") {
7325 return Err(self.err(alloc::format!(
7326 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7327 )));
7328 }
7329 return Ok(PlPgSqlStmt::While { condition, body });
7330 }
7331 // v7.12.6 — RAISE.
7332 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7333 {
7334 self.advance();
7335 return self.parse_plpgsql_raise();
7336 }
7337 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7338 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7339 {
7340 self.advance();
7341 let condition = self.parse_expr(0)?;
7342 let message = if matches!(self.peek(), Token::Comma) {
7343 self.advance();
7344 Some(self.parse_expr(0)?)
7345 } else {
7346 None
7347 };
7348 return Ok(PlPgSqlStmt::Assert { condition, message });
7349 }
7350 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7351 // "PERFORM is equivalent to SELECT but discards the
7352 // result." Side effects (function calls, RAISE inside
7353 // SQL functions, etc.) still execute. We desugar to
7354 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7355 // existing embedded-statement path handles execution +
7356 // result-discard cleanly. The result is naturally
7357 // discarded because EmbeddedSql doesn't propagate row
7358 // sets back to the plpgsql interpreter.
7359 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7360 {
7361 self.advance();
7362 // Splice a synthetic Token::Select into the stream at
7363 // the current position so parse_select_stmt parses the
7364 // remainder as a normal SELECT body. Token-stream
7365 // surgery mirrors the try_parse_plpgsql_select_into
7366 // pattern used for SELECT … INTO desugaring.
7367 self.tokens.insert(self.pos, Token::Select);
7368 let select = self.parse_select_stmt()?;
7369 let Statement::Select(s) = select else {
7370 return Err(self.err(alloc::format!(
7371 "expected SELECT body after PERFORM, got {:?}",
7372 self.peek()
7373 )));
7374 };
7375 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7376 }
7377 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7378 // plpgsql-specific shape (mailrs round-10 migrate-042).
7379 // PG's SELECT INTO at top-level SQL would CREATE a new
7380 // table; inside plpgsql it ASSIGNS the query result to
7381 // a local variable. We detect the INTO at paren-depth
7382 // 0 between SELECT and the statement boundary; if
7383 // found, split the token stream into "pre-INTO
7384 // projection" + "var" + "post-INTO FROM/WHERE…" and
7385 // rebuild as a SelectInto with a regular SELECT body
7386 // (no INTO clause).
7387 if matches!(self.peek(), Token::Select)
7388 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7389 {
7390 return Ok(PlPgSqlStmt::SelectInto {
7391 var: var_name,
7392 body: Box::new(select_body),
7393 });
7394 }
7395 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7396 // SELECT can appear directly inside a trigger body; we
7397 // recurse into the regular Statement parser, which will
7398 // stop at the trailing `;` (which our caller then
7399 // consumes).
7400 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7401 // also embed ALTER / CREATE / DROP statements; route
7402 // those through the same parser so the DO body parses
7403 // cleanly.
7404 if matches!(self.peek(), Token::Insert)
7405 || matches!(self.peek(), Token::Select)
7406 || matches!(self.peek(), Token::Create)
7407 || matches!(self.peek(), Token::Drop)
7408 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7409 if s.eq_ignore_ascii_case("update")
7410 || s.eq_ignore_ascii_case("delete")
7411 || s.eq_ignore_ascii_case("alter"))
7412 {
7413 let stmt = self.parse_one_statement()?;
7414 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7415 }
7416 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7417 // followed by `:=` and an expression.
7418 let target = self.parse_plpgsql_assign_target()?;
7419 // PL/pgSQL assignment uses `:=`. The lexer represents
7420 // this as a colon followed by `=`; check both shapes.
7421 match self.peek() {
7422 Token::ColonEq => {
7423 self.advance();
7424 }
7425 Token::Colon => {
7426 self.advance();
7427 if !matches!(self.peek(), Token::Eq) {
7428 return Err(self.err(alloc::format!(
7429 "expected := after plpgsql assign target, got `:` then {:?}",
7430 self.peek()
7431 )));
7432 }
7433 self.advance();
7434 }
7435 other => {
7436 return Err(self.err(alloc::format!(
7437 "expected := after plpgsql assign target, got {other:?}"
7438 )));
7439 }
7440 }
7441 let value = self.parse_expr(0)?;
7442 Ok(PlPgSqlStmt::Assign { target, value })
7443 }
7444
7445 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7446 /// [ELSE body] END IF`. `IF` keyword already consumed.
7447 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7448 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7449 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7450 loop {
7451 // <expr> THEN
7452 let cond = self.parse_expr(0)?;
7453 let then_kw = self.expect_ident_like()?;
7454 if !then_kw.eq_ignore_ascii_case("then") {
7455 return Err(self.err(alloc::format!(
7456 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7457 )));
7458 }
7459 let body = self.parse_plpgsql_stmt_list_until_end()?;
7460 branches.push((cond, body));
7461 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7462 match self.peek() {
7463 Token::Ident(s) | Token::QuotedIdent(s)
7464 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7465 {
7466 self.advance();
7467 continue;
7468 }
7469 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7470 self.advance();
7471 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7472 break;
7473 }
7474 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7475 break;
7476 }
7477 other => {
7478 return Err(self.err(alloc::format!(
7479 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7480 )));
7481 }
7482 }
7483 }
7484 // Expect `END IF` (the END keyword is the one we're
7485 // looking at right now).
7486 let end_kw = self.expect_ident_like()?;
7487 if !end_kw.eq_ignore_ascii_case("end") {
7488 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7489 }
7490 let if_kw = self.expect_ident_like()?;
7491 if !if_kw.eq_ignore_ascii_case("if") {
7492 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7493 }
7494 Ok(PlPgSqlStmt::If {
7495 branches,
7496 else_branch,
7497 })
7498 }
7499
7500 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7501 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7502 /// is already consumed.
7503 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7504 let lvl_ident = self.expect_ident_like()?;
7505 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7506 "notice" => RaiseLevel::Notice,
7507 "warning" => RaiseLevel::Warning,
7508 "info" => RaiseLevel::Info,
7509 "log" => RaiseLevel::Log,
7510 "debug" => RaiseLevel::Debug,
7511 "exception" => RaiseLevel::Exception,
7512 other => {
7513 return Err(self.err(alloc::format!(
7514 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7515 )));
7516 }
7517 };
7518 // Message: required for v7.12.6. PG accepts a bare
7519 // RAISE-rethrow form (no message), reserved for future
7520 // RAISE-no-args support.
7521 let Token::String(msg) = self.peek() else {
7522 return Err(self.err(alloc::format!(
7523 "expected RAISE message string, got {:?}",
7524 self.peek()
7525 )));
7526 };
7527 let message = msg.clone();
7528 self.advance();
7529 // Optional comma-separated args (PG `%` format substitution).
7530 let mut args: Vec<Expr> = Vec::new();
7531 while matches!(self.peek(), Token::Comma) {
7532 self.advance();
7533 args.push(self.parse_expr(0)?);
7534 }
7535 Ok(PlPgSqlStmt::Raise {
7536 level,
7537 message,
7538 args,
7539 })
7540 }
7541
7542 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7543 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7544 /// migrate-042). Returns `(rebuilt_select_without_into,
7545 /// var_name)` when the pattern matches; `None` for
7546 /// regular SELECTs (those go through the embedded-SQL
7547 /// path). Token-stream surgery so the rebuilt SELECT
7548 /// parses through the regular `parse_select_stmt`.
7549 #[allow(clippy::too_many_lines)]
7550 fn try_parse_plpgsql_select_into(
7551 &mut self,
7552 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7553 // Scan forward from `self.pos + 1` (past Token::Select)
7554 // for Token::Into at paren-depth 0, stopping at the
7555 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7556 // end the plpgsql statement.
7557 let start = self.pos;
7558 let mut into_pos: Option<usize> = None;
7559 let mut depth: i32 = 0;
7560 let mut i = start + 1;
7561 while i < self.tokens.len() {
7562 match &self.tokens[i] {
7563 Token::LParen => depth += 1,
7564 Token::RParen => depth -= 1,
7565 Token::Semicolon if depth == 0 => break,
7566 Token::Ident(s)
7567 if depth == 0
7568 && (s.eq_ignore_ascii_case("end")
7569 || s.eq_ignore_ascii_case("else")
7570 || s.eq_ignore_ascii_case("elsif")) =>
7571 {
7572 break;
7573 }
7574 Token::Into if depth == 0 => {
7575 into_pos = Some(i);
7576 break;
7577 }
7578 _ => {}
7579 }
7580 i += 1;
7581 }
7582 let Some(into_at) = into_pos else {
7583 return Ok(None);
7584 };
7585 // The token immediately after INTO must be the target
7586 // var ident; anything else (e.g. INSERT INTO table)
7587 // ruled out by the depth-0 check above. Capture it.
7588 let var = match self.tokens.get(into_at + 1) {
7589 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7590 other => {
7591 return Err(self.err(alloc::format!(
7592 "expected variable name after SELECT … INTO, got {other:?}"
7593 )));
7594 }
7595 };
7596 // Find the end of the plpgsql SELECT INTO statement —
7597 // same boundary rules as the depth-0 scan above.
7598 let mut end = into_at + 2;
7599 let mut depth2: i32 = 0;
7600 while end < self.tokens.len() {
7601 match &self.tokens[end] {
7602 Token::LParen => depth2 += 1,
7603 Token::RParen => depth2 -= 1,
7604 Token::Semicolon if depth2 == 0 => break,
7605 Token::Ident(s)
7606 if depth2 == 0
7607 && (s.eq_ignore_ascii_case("end")
7608 || s.eq_ignore_ascii_case("else")
7609 || s.eq_ignore_ascii_case("elsif")) =>
7610 {
7611 break;
7612 }
7613 _ => {}
7614 }
7615 end += 1;
7616 }
7617 // Rebuild a token stream that represents the SELECT
7618 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7619 // post-var tokens up to statement end]. Run the
7620 // regular `parse_select_stmt` against it.
7621 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7622 for j in start..into_at {
7623 rebuilt.push(self.tokens[j].clone());
7624 }
7625 for j in (into_at + 2)..end {
7626 rebuilt.push(self.tokens[j].clone());
7627 }
7628 rebuilt.push(Token::Eof);
7629 let saved_pos = self.pos;
7630 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7631 self.pos = 0;
7632 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7633 if !matches!(self.peek(), Token::Select) {
7634 self.tokens = saved_tokens;
7635 self.pos = saved_pos;
7636 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7637 }
7638 let sel = self.parse_select_stmt();
7639 self.tokens = saved_tokens;
7640 self.pos = end;
7641 let sel = sel?;
7642 let Statement::Select(body) = sel else {
7643 return Err(self.err(alloc::format!(
7644 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7645 )));
7646 };
7647 Ok(Some((body, var)))
7648 }
7649
7650 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7651 // v7.16.1 — read the head token DIRECTLY rather than
7652 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7653 // strip (`public.t` → `t`) inside `expect_ident_like`
7654 // greedily consumes any `ident . ident` pair, which
7655 // silently turned every `NEW.col := …` /
7656 // `OLD.col := …` plpgsql assignment into a Local("col")
7657 // assignment — the head "new"/"old" was eaten as if it
7658 // were a schema name and the Dot was consumed too, so
7659 // this function's own `peek() == Token::Dot` check
7660 // below never fired. Every BEFORE trigger that rewrote
7661 // a NEW cell was a silent no-op for two major releases
7662 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7663 // gate failures were investigated as v7.16.1 backlog.
7664 let head = match self.advance() {
7665 Token::Ident(s) | Token::QuotedIdent(s) => s,
7666 other => {
7667 return Err(self.err(alloc::format!(
7668 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7669 )));
7670 }
7671 };
7672 if matches!(self.peek(), Token::Dot) {
7673 self.advance();
7674 let col = self.expect_ident_like()?;
7675 if head.eq_ignore_ascii_case("new") {
7676 return Ok(AssignTarget::NewColumn(col));
7677 }
7678 if head.eq_ignore_ascii_case("old") {
7679 return Ok(AssignTarget::OldColumn(col));
7680 }
7681 return Err(self.err(alloc::format!(
7682 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7683 got {head:?}.<col>"
7684 )));
7685 }
7686 Ok(AssignTarget::Local(head))
7687 }
7688
7689 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7690 // RETURN NEW / OLD / NULL — bare-ident forms.
7691 match self.peek() {
7692 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7693 self.advance();
7694 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7695 }
7696 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7697 self.advance();
7698 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7699 }
7700 Token::Null => {
7701 self.advance();
7702 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7703 }
7704 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7705 // per PL/pgSQL convention.
7706 Token::Semicolon => {
7707 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7708 }
7709 _ => {}
7710 }
7711 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7712 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7713 // caller-visible effect (blocks don't return sets), so we
7714 // desugar it identically to PERFORM: parse the SELECT (or
7715 // EXECUTE dynamic) as embedded SQL that runs for side
7716 // effects and discards the result. RETURN NEXT <expr>
7717 // (single-row accumulator) queues with v7.40 SETOF function
7718 // infrastructure.
7719 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7720 // and keep going.
7721 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7722 {
7723 self.advance();
7724 let e = self.parse_expr(0)?;
7725 return Ok(PlPgSqlStmt::ReturnNext(e));
7726 }
7727 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7728 {
7729 self.advance();
7730 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7731 // rows go to the set, like the static form. It used to desugar to a
7732 // bare ExecuteDynamic, whose result was DISCARDED.
7733 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7734 {
7735 self.advance();
7736 let sql = self.parse_expr(0)?;
7737 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7738 }
7739 // Bare RETURN QUERY <select>. If the current token is
7740 // not already SELECT (e.g., the user wrote `RETURN QUERY
7741 // <projection> FROM ...` in a shorthand — rare but PG
7742 // accepts a bare projection here), splice one in. Same
7743 // trick as PERFORM.
7744 if !matches!(self.peek(), Token::Select) {
7745 self.tokens.insert(self.pos, Token::Select);
7746 }
7747 let select = self.parse_select_stmt()?;
7748 let Statement::Select(s) = select else {
7749 return Err(self.err(alloc::format!(
7750 "expected SELECT body after RETURN QUERY, got {:?}",
7751 self.peek()
7752 )));
7753 };
7754 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7755 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7756 // in a SETOF function is the entire answer thrown away.
7757 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7758 }
7759 // Fall through: parse a full expression.
7760 let e = self.parse_expr(0)?;
7761 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7762 }
7763
7764 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7765 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7766 // are ident-shaped (the parser keys off case-insensitive
7767 // match — same shape used by the top-level Update / Delete
7768 // dispatchers at parse_one_statement).
7769 if matches!(self.peek(), Token::Insert) {
7770 self.advance();
7771 return Ok(TriggerEvent::Insert);
7772 }
7773 match self.peek() {
7774 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7775 self.advance();
7776 Ok(TriggerEvent::Update)
7777 }
7778 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7779 self.advance();
7780 Ok(TriggerEvent::Delete)
7781 }
7782 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7783 self.advance();
7784 Ok(TriggerEvent::Truncate)
7785 }
7786 other => Err(self.err(alloc::format!(
7787 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7788 ))),
7789 }
7790 }
7791
7792 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7793 /// - (no clause) → implicit `FOR ALL TABLES`
7794 /// - `FOR ALL TABLES`
7795 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7796 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7797 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7798 /// REJECTS the bare plural (`invalid publication object list`,
7799 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7800 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7801 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7802 let name = self.expect_ident_or_string()?;
7803 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7804 // shape so existing publications keep parsing identically.
7805 let scope = if matches!(self.peek(), Token::For) {
7806 self.advance();
7807 if matches!(self.peek(), Token::All) {
7808 self.advance();
7809 if !matches!(self.peek(), Token::Tables) {
7810 return Err(self.err(format!(
7811 "expected TABLES after FOR ALL, got {:?}",
7812 self.peek()
7813 )));
7814 }
7815 self.advance();
7816 if matches!(self.peek(), Token::Except) {
7817 self.advance();
7818 let tables = self.parse_publication_table_list()?;
7819 PublicationScope::AllTablesExcept(tables)
7820 } else {
7821 PublicationScope::AllTables
7822 }
7823 } else if matches!(self.peek(), Token::Table) {
7824 self.advance();
7825 let tables = self.parse_publication_table_list()?;
7826 PublicationScope::ForTables(tables)
7827 } else if matches!(self.peek(), Token::Tables) {
7828 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7829 // plural (`FOR TABLES t`) is REJECTED (`invalid
7830 // publication object list`); TABLES only pairs with
7831 // `IN SCHEMA`. The old arm accepted it on an
7832 // unverifiable "PG 19 accepts both" claim.
7833 self.advance();
7834 if !matches!(self.peek(), Token::In) {
7835 return Err(self.err(alloc::string::String::from(
7836 "invalid publication object list",
7837 )));
7838 }
7839 self.advance();
7840 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7841 return Err(self.err(format!(
7842 "expected SCHEMA after FOR TABLES IN, got {:?}",
7843 self.peek()
7844 )));
7845 }
7846 self.advance();
7847 let schema = self.expect_ident_or_string()?;
7848 PublicationScope::TablesInSchema(schema)
7849 } else {
7850 return Err(self.err(format!(
7851 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7852 self.peek()
7853 )));
7854 }
7855 } else {
7856 PublicationScope::AllTables
7857 };
7858 Ok(Statement::CreatePublication(CreatePublicationStatement {
7859 name,
7860 scope,
7861 }))
7862 }
7863
7864 /// v6.1.3 — Comma-separated identifier list for the publication
7865 /// FOR-clause. Requires at least one entry; empty list is a
7866 /// parse error (PG behaviour). Quoted idents are accepted; the
7867 /// names round-trip through `Display` as `quote_ident(name)`.
7868 ///
7869 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7870 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7871 /// pg_dump output. SPG's publication state today is per-table
7872 /// only (matching the pre-PG-15 surface); the col list + WHERE
7873 /// are parsed so dumps load through and the table name reaches
7874 /// `PublicationScope::ForTables`, but the filter is not enforced
7875 /// at publish time. Re-open when a customer dogfood gate
7876 /// requires per-row-filter or column-subset publish semantics
7877 /// (which gates on persistent slot state landing first, 21.12).
7878 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7879 let first = self.parse_publication_table_entry()?;
7880 let mut out = alloc::vec![first];
7881 while matches!(self.peek(), Token::Comma) {
7882 self.advance();
7883 out.push(self.parse_publication_table_entry()?);
7884 }
7885 Ok(out)
7886 }
7887
7888 /// One table entry inside a FOR TABLE clause:
7889 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7890 /// Returns just the table name; the column list + WHERE predicate
7891 /// are consumed and discarded per the parse-accept-discard
7892 /// commitment above.
7893 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7894 let name = self.expect_ident_like()?;
7895 // Optional column list — `(col, col, …)`.
7896 if matches!(self.peek(), Token::LParen) {
7897 self.advance();
7898 // Empty parens are a PG error too; require ≥ 1 column.
7899 let _ = self.expect_ident_like()?;
7900 while matches!(self.peek(), Token::Comma) {
7901 self.advance();
7902 let _ = self.expect_ident_like()?;
7903 }
7904 if !matches!(self.peek(), Token::RParen) {
7905 return Err(self.err(alloc::format!(
7906 "expected ')' to close publication column list, got {:?}",
7907 self.peek()
7908 )));
7909 }
7910 self.advance();
7911 }
7912 // Optional row filter — `WHERE (predicate)`.
7913 if matches!(self.peek(), Token::Where) {
7914 self.advance();
7915 if !matches!(self.peek(), Token::LParen) {
7916 return Err(self.err(alloc::format!(
7917 "expected '(' after WHERE in publication row filter, got {:?}",
7918 self.peek()
7919 )));
7920 }
7921 self.advance();
7922 let _ = self.parse_expr(0)?;
7923 if !matches!(self.peek(), Token::RParen) {
7924 return Err(self.err(alloc::format!(
7925 "expected ')' to close publication WHERE filter, got {:?}",
7926 self.peek()
7927 )));
7928 }
7929 self.advance();
7930 }
7931 Ok(name)
7932 }
7933
7934 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7935 /// CONNECTION '<conn>'
7936 /// PUBLICATION <pub> [, <pub> ...]`.
7937 ///
7938 /// The clause order is fixed (CONNECTION first, then
7939 /// PUBLICATION) to match PG. No WITH-options accepted in
7940 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7941 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7942 let name = self.expect_ident_or_string()?;
7943 if !matches!(self.peek(), Token::Connection) {
7944 return Err(self.err(format!(
7945 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7946 self.peek()
7947 )));
7948 }
7949 self.advance();
7950 let conn_str = self.expect_string_literal()?;
7951 if !matches!(self.peek(), Token::Publication) {
7952 return Err(self.err(format!(
7953 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7954 self.peek()
7955 )));
7956 }
7957 self.advance();
7958 // Reuse the publication FOR-list parser shape: at least one
7959 // identifier, comma-separated.
7960 let first = self.expect_ident_like()?;
7961 let mut publications = alloc::vec![first];
7962 while matches!(self.peek(), Token::Comma) {
7963 self.advance();
7964 publications.push(self.expect_ident_like()?);
7965 }
7966 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7967 name,
7968 conn_str,
7969 publications,
7970 }))
7971 }
7972
7973 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7974 /// All keywords after `WAIT` are bare idents in v6.1.x; no
7975 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7976 /// that fit `u64`.
7977 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7978 /// qualifier is a *namespace* the app owns (`app.user_id`,
7979 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7980 /// to discard. So parse the raw segments here instead of
7981 /// `expect_ident_like`, which strips a leading `schema.` qualifier
7982 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7983 /// a single segment and round-trip unchanged.
7984 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7985 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7986 loop {
7987 let seg = match self.advance() {
7988 Token::Ident(s) | Token::QuotedIdent(s) => s,
7989 other if unreserved_keyword_text(&other).is_some() => {
7990 unreserved_keyword_text(&other).unwrap()
7991 }
7992 other => {
7993 return Err(ParseError {
7994 message: format!("expected parameter name, got {other:?}"),
7995 token_pos: self.consumed_pos(),
7996 });
7997 }
7998 };
7999 parts.push(seg);
8000 if matches!(self.peek(), Token::Dot) {
8001 self.advance();
8002 continue;
8003 }
8004 break;
8005 }
8006 Ok(parts.join(".").to_ascii_lowercase())
8007 }
8008
8009 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8010 Self::parse_set_value_inner(self)
8011 }
8012
8013 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8014 match self.advance() {
8015 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8016 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8017 Ok(crate::ast::SetValue::Default)
8018 }
8019 Token::Ident(s) | Token::QuotedIdent(s) => {
8020 let mut accum = s;
8021 while matches!(self.peek(), Token::Dot) {
8022 self.advance();
8023 let next = self.expect_ident_like()?;
8024 accum.push('.');
8025 accum.push_str(&next);
8026 }
8027 Ok(crate::ast::SetValue::Ident(accum))
8028 }
8029 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8030 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8031 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8032 // spellings that lex as keyword tokens, not idents:
8033 // `SET standard_conforming_strings = on` is in every
8034 // pg_dump preamble (`off` already lexes as an ident).
8035 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8036 // DEFAULT lexes as its keyword token, so the ident arm above
8037 // never saw it and the everyday reset form was a syntax error.
8038 Token::Default => Ok(crate::ast::SetValue::Default),
8039 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8040 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8041 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8042 // v7.14.0 — MySQL session/user variable RHS
8043 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8044 // Wrap as Ident so the SET handler can record it; the
8045 // engine treats `@VAR` / `@@VAR` values as opaque
8046 // strings.
8047 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8048 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8049 // is the common MySQL preamble shape. Allow a `+` or
8050 // `-` prefix on negative numerics for parity with PG
8051 // (some param defaults are negative).
8052 Token::Minus => match self.advance() {
8053 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8054 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8055 other => Err(self.err(format!(
8056 "expected numeric after `-` in SET value, got {other:?}"
8057 ))),
8058 },
8059 other => Err(self.err(format!(
8060 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8061 ))),
8062 }
8063 }
8064
8065 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8066 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8067 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8068 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8069 /// present). Modes are comma-separated per PG; SPG also
8070 /// accepts space-separated for tolerance. READ ONLY / WRITE
8071 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8072 /// surface but not behaviorally honoured today).
8073 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8074 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8075 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8076 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8077 /// session default rather than forcing READ COMMITTED.
8078 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8079 let mut level = IsolationLevel::default();
8080 let mut have_level = false;
8081 loop {
8082 // ISOLATION LEVEL …
8083 let saw_isolation =
8084 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8085 if saw_isolation {
8086 self.advance(); // ISOLATION
8087 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8088 return Err(self.err(alloc::format!(
8089 "expected LEVEL after ISOLATION, got {:?}",
8090 self.peek()
8091 )));
8092 }
8093 self.advance(); // LEVEL
8094 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8095 let w1 = self
8096 .expect_ident_like()
8097 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8098 let lc = w1.to_ascii_lowercase();
8099 level = match lc.as_str() {
8100 "serializable" => IsolationLevel::Serializable,
8101 "repeatable" => {
8102 // Expect READ
8103 let w2 = self
8104 .expect_ident_like()
8105 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8106 if !w2.eq_ignore_ascii_case("read") {
8107 return Err(self.err(alloc::format!(
8108 "expected READ after REPEATABLE, got {w2:?}"
8109 )));
8110 }
8111 IsolationLevel::RepeatableRead
8112 }
8113 "read" => {
8114 let w2 = self
8115 .expect_ident_like()
8116 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8117 match w2.to_ascii_lowercase().as_str() {
8118 "committed" => IsolationLevel::ReadCommitted,
8119 "uncommitted" => IsolationLevel::ReadUncommitted,
8120 other => {
8121 return Err(self.err(alloc::format!(
8122 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8123 )));
8124 }
8125 }
8126 }
8127 other => {
8128 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8129 }
8130 };
8131 have_level = true;
8132 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8133 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8134 self.advance();
8135 match self.peek().clone() {
8136 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8137 self.advance();
8138 }
8139 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8140 self.advance();
8141 }
8142 other => {
8143 return Err(self.err(alloc::format!(
8144 "expected ONLY or WRITE after READ, got {other:?}"
8145 )));
8146 }
8147 }
8148 } else if matches!(self.peek(), Token::Not) {
8149 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8150 self.advance();
8151 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8152 return Err(self.err(alloc::format!(
8153 "expected DEFERRABLE after NOT, got {:?}",
8154 self.peek()
8155 )));
8156 }
8157 self.advance();
8158 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8159 {
8160 self.advance();
8161 } else {
8162 break;
8163 }
8164 // Optional comma between modes.
8165 if matches!(self.peek(), Token::Comma) {
8166 self.advance();
8167 }
8168 }
8169 Ok(have_level.then_some(level))
8170 }
8171
8172 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8173 // FOR is a v6.1.2-reserved keyword (Token::For). The
8174 // other two are bare idents — they've never needed lexer
8175 // support and we keep it that way.
8176 if !matches!(self.peek(), Token::For) {
8177 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8178 }
8179 self.advance();
8180 self.expect_keyword_ident("wal")?;
8181 self.expect_keyword_ident("position")?;
8182 let pos = self.expect_u64_literal()?;
8183 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8184 {
8185 self.advance();
8186 self.expect_keyword_ident("timeout")?;
8187 Some(self.expect_u64_literal()?)
8188 } else {
8189 None
8190 };
8191 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8192 }
8193
8194 /// v6.1.7 helper — consume a `Token::Integer` and check it
8195 /// fits `u64`. WAL positions and millisecond timeouts are
8196 /// non-negative.
8197 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8198 match self.advance() {
8199 Token::Integer(n) if n >= 0 => Ok(n as u64),
8200 Token::Integer(n) => Err(ParseError {
8201 message: format!("expected non-negative integer, got {n}"),
8202 token_pos: self.consumed_pos(),
8203 }),
8204 other => Err(ParseError {
8205 message: format!("expected integer literal, got {other:?}"),
8206 token_pos: self.consumed_pos(),
8207 }),
8208 }
8209 }
8210
8211 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8212 /// ROLE '<role>' (defaults to readonly). All string slots accept
8213 /// either a quoted ident or a quoted string literal.
8214 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8215 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8216 ///
8217 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8218 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8219 /// wire role) still parses — it is a different axis from the PG attributes.
8220 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8221 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8222 /// or RESET, so the plain attribute forms keep their old path.
8223 fn peeks_db_role_setting(&self) -> bool {
8224 let mut i = self.pos + 1; // past the object's name
8225 let word = |p: usize| -> Option<String> {
8226 match self.tokens.get(p) {
8227 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8228 Some(Token::In) => Some(String::from("in")),
8229 _ => None,
8230 }
8231 };
8232 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8233 i += 3; // IN DATABASE <name>
8234 }
8235 matches!(word(i).as_deref(), Some("set" | "reset"))
8236 }
8237
8238 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8239 use crate::ast::SetDbRoleSettingStatement;
8240 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8241 // identifier, so the ordinary name reader refuses it. Same trap
8242 // as TABLE / INDEX / FULL / DEFAULT before it.
8243 let name = if matches!(self.peek(), Token::All) {
8244 self.advance();
8245 String::from("all")
8246 } else {
8247 self.expect_ident_or_string()?
8248 };
8249 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8250 let all = name.eq_ignore_ascii_case("all");
8251 let (mut database, mut role) = if is_database {
8252 (Some(name), None)
8253 } else if all {
8254 (None, None)
8255 } else {
8256 (None, Some(name))
8257 };
8258 if matches!(self.peek(), Token::In) {
8259 self.advance();
8260 self.advance(); // DATABASE
8261 database = Some(self.expect_ident_or_string()?);
8262 }
8263 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8264 self.advance(); // SET | RESET
8265 if resetting && matches!(self.peek(), Token::All) {
8266 self.advance();
8267 self.consume_until_statement_boundary();
8268 return Ok(Statement::SetDbRoleSetting(Box::new(
8269 SetDbRoleSettingStatement {
8270 database,
8271 role,
8272 param: None,
8273 value: None,
8274 },
8275 )));
8276 }
8277 let param = self.expect_ident_like()?;
8278 let value = if resetting {
8279 None
8280 } else {
8281 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8282 // KEYWORD, so the ident-only check missed it and consumed
8283 // the word itself as the value — the same trap as ALL, one
8284 // clause over.
8285 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8286 self.advance();
8287 }
8288 Some(self.take_guc_value())
8289 };
8290 self.consume_until_statement_boundary();
8291 Ok(Statement::SetDbRoleSetting(Box::new(
8292 SetDbRoleSettingStatement {
8293 database,
8294 role,
8295 param: Some(param),
8296 value,
8297 },
8298 )))
8299 }
8300
8301 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8302 /// a quoted literal loses its quotes, a bare word or number does not.
8303 fn take_guc_value(&mut self) -> String {
8304 match self.advance() {
8305 Token::String(s) => s,
8306 Token::Integer(n) => format!("{n}"),
8307 Token::Float(f) => format!("{f}"),
8308 Token::Ident(s) | Token::QuotedIdent(s) => s,
8309 other => format!("{other:?}"),
8310 }
8311 }
8312
8313 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8314 let name = self.expect_ident_or_string()?;
8315 if self.peek_keyword_ident("with") {
8316 self.advance();
8317 }
8318 let mut password = String::new();
8319 let mut role = String::new();
8320 let mut login: Option<bool> = None;
8321 let mut inherit: Option<bool> = None;
8322 let mut superuser: Option<bool> = None;
8323 // Not a `while let`: the pattern would borrow `self` across the
8324 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8325 #[allow(clippy::while_let_loop)]
8326 loop {
8327 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8328 break;
8329 };
8330 match w.to_ascii_lowercase().as_str() {
8331 "password" => {
8332 self.advance();
8333 password = self.expect_string_literal()?;
8334 }
8335 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8336 // is the same slot.
8337 "encrypted" => {
8338 self.advance();
8339 self.expect_keyword_ident("password")?;
8340 password = self.expect_string_literal()?;
8341 }
8342 "login" => {
8343 self.advance();
8344 login = Some(true);
8345 }
8346 "nologin" => {
8347 self.advance();
8348 login = Some(false);
8349 }
8350 "inherit" => {
8351 self.advance();
8352 inherit = Some(true);
8353 }
8354 "noinherit" => {
8355 self.advance();
8356 inherit = Some(false);
8357 }
8358 "superuser" => {
8359 self.advance();
8360 superuser = Some(true);
8361 }
8362 "nosuperuser" => {
8363 self.advance();
8364 superuser = Some(false);
8365 }
8366 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8367 "role" => {
8368 self.advance();
8369 role = self.expect_string_literal()?;
8370 }
8371 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8372 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8373 // accepted and ignored so a pg_dump role block restores. They
8374 // gate capabilities SPG does not have.
8375 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8376 | "noreplication" | "bypassrls" | "nobypassrls" => {
8377 self.advance();
8378 }
8379 "connection" => {
8380 self.advance();
8381 self.expect_keyword_ident("limit")?;
8382 self.advance(); // the number
8383 }
8384 "valid" => {
8385 self.advance();
8386 self.expect_keyword_ident("until")?;
8387 self.expect_string_literal()?;
8388 }
8389 _ => break,
8390 }
8391 }
8392 if role.is_empty() {
8393 role = "readonly".to_string();
8394 }
8395 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8396 name,
8397 password,
8398 role,
8399 login,
8400 inherit,
8401 superuser,
8402 is_user,
8403 }))
8404 }
8405
8406 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8407 /// consumed the USING / WITH CHECK keyword.
8408 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8409 if !matches!(self.peek(), Token::LParen) {
8410 return Err(self.err(alloc::format!(
8411 "expected '(' after {clause}, got {:?}",
8412 self.peek()
8413 )));
8414 }
8415 self.advance();
8416 let e = self.parse_expr(0)?;
8417 if !matches!(self.peek(), Token::RParen) {
8418 return Err(self.err(alloc::format!(
8419 "expected ')' to close {clause}, got {:?}",
8420 self.peek()
8421 )));
8422 }
8423 self.advance();
8424 Ok(e)
8425 }
8426
8427 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8428 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8429 let mut roles = Vec::new();
8430 loop {
8431 roles.push(self.expect_ident_like()?);
8432 if matches!(self.peek(), Token::Comma) {
8433 self.advance();
8434 } else {
8435 break;
8436 }
8437 }
8438 Ok(roles)
8439 }
8440
8441 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8442 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8443 /// `CREATE POLICY`.
8444 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8445 use crate::ast::PolicyCmd;
8446 let name = self.expect_ident_like()?;
8447 if !matches!(self.peek(), Token::On) {
8448 return Err(self.err(alloc::format!(
8449 "expected ON after CREATE POLICY name, got {:?}",
8450 self.peek()
8451 )));
8452 }
8453 self.advance();
8454 let table = self.expect_ident_like()?;
8455
8456 let mut permissive = true;
8457 if matches!(self.peek(), Token::As) {
8458 self.advance();
8459 let w = self.expect_ident_like()?;
8460 permissive = if w.eq_ignore_ascii_case("permissive") {
8461 true
8462 } else if w.eq_ignore_ascii_case("restrictive") {
8463 false
8464 } else {
8465 return Err(self.err(alloc::format!(
8466 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8467 )));
8468 };
8469 }
8470
8471 let mut cmd = PolicyCmd::All;
8472 if matches!(self.peek(), Token::For) {
8473 self.advance();
8474 cmd = self.parse_policy_cmd()?;
8475 }
8476
8477 let mut roles = Vec::new();
8478 if matches!(self.peek(), Token::To) {
8479 self.advance();
8480 roles = self.parse_policy_roles()?;
8481 }
8482
8483 let mut using = None;
8484 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8485 {
8486 self.advance();
8487 using = Some(self.parse_paren_expr("USING")?);
8488 }
8489
8490 let mut with_check = None;
8491 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8492 {
8493 self.advance();
8494 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8495 {
8496 return Err(self.err(alloc::format!(
8497 "expected CHECK after WITH, got {:?}",
8498 self.peek()
8499 )));
8500 }
8501 self.advance();
8502 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8503 }
8504
8505 // Clause-per-command matrix (PG wording).
8506 match cmd {
8507 PolicyCmd::Insert => {
8508 if using.is_some() {
8509 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8510 }
8511 }
8512 PolicyCmd::Select | PolicyCmd::Delete => {
8513 if with_check.is_some() {
8514 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8515 }
8516 }
8517 PolicyCmd::Update | PolicyCmd::All => {}
8518 }
8519
8520 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8521 name,
8522 table,
8523 permissive,
8524 cmd,
8525 roles,
8526 using,
8527 with_check,
8528 }))
8529 }
8530
8531 /// v7.39 (RLS) — the command word after `FOR`.
8532 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8533 use crate::ast::PolicyCmd;
8534 match self.peek().clone() {
8535 Token::All => {
8536 self.advance();
8537 Ok(PolicyCmd::All)
8538 }
8539 Token::Select => {
8540 self.advance();
8541 Ok(PolicyCmd::Select)
8542 }
8543 Token::Insert => {
8544 self.advance();
8545 Ok(PolicyCmd::Insert)
8546 }
8547 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8548 self.advance();
8549 Ok(PolicyCmd::Update)
8550 }
8551 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8552 self.advance();
8553 Ok(PolicyCmd::Delete)
8554 }
8555 other => Err(self.err(alloc::format!(
8556 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8557 ))),
8558 }
8559 }
8560
8561 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8562 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8563 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8564 let name = self.expect_ident_like()?;
8565 if !matches!(self.peek(), Token::On) {
8566 return Err(self.err(alloc::format!(
8567 "expected ON after ALTER POLICY name, got {:?}",
8568 self.peek()
8569 )));
8570 }
8571 self.advance();
8572 let table = self.expect_ident_like()?;
8573
8574 // RENAME TO new
8575 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8576 {
8577 self.advance();
8578 if !matches!(self.peek(), Token::To) {
8579 return Err(self.err(alloc::format!(
8580 "expected TO after RENAME, got {:?}",
8581 self.peek()
8582 )));
8583 }
8584 self.advance();
8585 let new = self.expect_ident_like()?;
8586 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8587 name,
8588 table,
8589 rename_to: Some(new),
8590 roles: None,
8591 using: None,
8592 with_check: None,
8593 }));
8594 }
8595
8596 let mut roles = None;
8597 if matches!(self.peek(), Token::To) {
8598 self.advance();
8599 roles = Some(self.parse_policy_roles()?);
8600 }
8601 let mut using = None;
8602 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8603 {
8604 self.advance();
8605 using = Some(self.parse_paren_expr("USING")?);
8606 }
8607 let mut with_check = None;
8608 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8609 {
8610 self.advance();
8611 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8612 {
8613 return Err(self.err(alloc::format!(
8614 "expected CHECK after WITH, got {:?}",
8615 self.peek()
8616 )));
8617 }
8618 self.advance();
8619 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8620 }
8621 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8622 name,
8623 table,
8624 rename_to: None,
8625 roles,
8626 using,
8627 with_check,
8628 }))
8629 }
8630
8631 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8632 /// `DROP POLICY`.
8633 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8634 let if_exists = self.consume_if_exists();
8635 let name = self.expect_ident_like()?;
8636 if !matches!(self.peek(), Token::On) {
8637 return Err(self.err(alloc::format!(
8638 "expected ON after DROP POLICY name, got {:?}",
8639 self.peek()
8640 )));
8641 }
8642 self.advance();
8643 let table = self.expect_ident_like()?;
8644 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8645 name,
8646 table,
8647 if_exists,
8648 }))
8649 }
8650}
8651fn wrap_from_leaves(
8652 e: &mut Expr,
8653 names: &[String],
8654 make: &dyn Fn(Expr) -> Expr,
8655 refs: &dyn Fn(&Expr) -> bool,
8656) {
8657 if let Expr::Column(c) = e {
8658 if c.qualifier
8659 .as_deref()
8660 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8661 {
8662 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8663 *e = make(taken);
8664 }
8665 return;
8666 }
8667 match e {
8668 Expr::Binary { lhs, rhs, .. } => {
8669 wrap_from_leaves(lhs, names, make, refs);
8670 wrap_from_leaves(rhs, names, make, refs);
8671 }
8672 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8673 wrap_from_leaves(expr, names, make, refs)
8674 }
8675 Expr::FunctionCall { args, .. } => {
8676 for a in args.iter_mut() {
8677 wrap_from_leaves(a, names, make, refs);
8678 }
8679 }
8680 Expr::Case {
8681 operand,
8682 branches,
8683 else_branch,
8684 } => {
8685 if let Some(o) = operand.as_deref_mut() {
8686 wrap_from_leaves(o, names, make, refs);
8687 }
8688 for (w, t) in branches.iter_mut() {
8689 wrap_from_leaves(w, names, make, refs);
8690 wrap_from_leaves(t, names, make, refs);
8691 }
8692 if let Some(el) = else_branch.as_deref_mut() {
8693 wrap_from_leaves(el, names, make, refs);
8694 }
8695 }
8696 // Compound variants the walk doesn't decompose: keep the
8697 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8698 // a source table, so nothing regresses.
8699 other => {
8700 if refs(other) {
8701 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8702 *other = make(taken);
8703 }
8704 }
8705 }
8706}
8707
8708/// v7.39 (round 241) — does this expression reference any of the FROM /
8709/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8710/// lowerings)?
8711fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8712 match e {
8713 Expr::Column(c) => c
8714 .qualifier
8715 .as_deref()
8716 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8717 Expr::Binary { lhs, rhs, .. } => {
8718 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8719 }
8720 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8721 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8722 Expr::Case {
8723 operand,
8724 branches,
8725 else_branch,
8726 } => {
8727 operand
8728 .as_deref()
8729 .is_some_and(|o| expr_refs_tables(o, names))
8730 || branches
8731 .iter()
8732 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8733 || else_branch
8734 .as_deref()
8735 .is_some_and(|el| expr_refs_tables(el, names))
8736 }
8737 _ => false,
8738 }
8739}
8740
8741impl Parser {
8742 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8743 /// Caller already consumed the leading `UPDATE` ident.
8744 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8745 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8746 /// after the target name has been read. `JOIN` is a reserved token;
8747 /// the qualifiers are bare idents.
8748 fn peek_is_update_join_start(&self) -> bool {
8749 match self.peek() {
8750 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8751 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8752 Token::Join
8753 | Token::Inner
8754 | Token::Left
8755 | Token::Right
8756 | Token::Cross
8757 | Token::Full => true,
8758 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8759 Token::Ident(s) | Token::QuotedIdent(s) => {
8760 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8761 }
8762 _ => false,
8763 }
8764 }
8765
8766 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8767 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8768 /// expression on the right, and `:=` as a second spelling of `=`.
8769 ///
8770 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8771 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8772 /// and holding this loop's `Vec` + `String` locals there overflowed the
8773 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8774 #[inline(never)]
8775 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8776 let mut assigns: Vec<(String, Expr)> = Vec::new();
8777 let mut settings: Vec<(String, Expr)> = Vec::new();
8778 loop {
8779 // v7.39 (round 554) — a plain NAME here is a session
8780 // setting, not a user variable. mysqldump writes the two in
8781 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8782 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8783 // changes it — and this refused the mixture outright, so no
8784 // dump could be restored past its preamble.
8785 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8786 self.advance();
8787 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8788 return Err(self.err(alloc::format!(
8789 "expected `=` after {name}, got {:?}",
8790 self.peek()
8791 )));
8792 }
8793 self.advance();
8794 let value = self.parse_expr(0)?;
8795 settings.push((name.to_ascii_lowercase(), value));
8796 if matches!(self.peek(), Token::Comma) {
8797 self.advance();
8798 continue;
8799 }
8800 break;
8801 }
8802 let Token::SessionVar(raw) = self.peek().clone() else {
8803 return Err(self.err(alloc::format!(
8804 "expected a user variable after SET, got {:?}",
8805 self.peek()
8806 )));
8807 };
8808 if raw.starts_with("@@") {
8809 return Err(self.err(alloc::string::String::from(
8810 "cannot mix `@@` settings with `@` user variables in one SET",
8811 )));
8812 }
8813 self.advance();
8814 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8815 return Err(self.err(alloc::format!(
8816 "expected `=` or `:=` after {raw}, got {:?}",
8817 self.peek()
8818 )));
8819 }
8820 self.advance();
8821 let value = self.parse_expr(0)?;
8822 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8823 if matches!(self.peek(), Token::Comma) {
8824 self.advance();
8825 continue;
8826 }
8827 break;
8828 }
8829 Ok(Statement::SetUserVars(assigns, settings))
8830 }
8831
8832 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8833 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8834 // NAMED `only` until now, which failed on `relation "only" does
8835 // not exist`. The lookahead is what keeps a table actually
8836 // called `only` working: the keyword is only a keyword when a
8837 // TABLE NAME follows it — and `SET` arrives as an identifier
8838 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8839 // for the table and die on the `=`. Measured by the pin.
8840 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8841 if s.eq_ignore_ascii_case("only"))
8842 && matches!(
8843 self.tokens.get(self.pos + 1),
8844 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8845 );
8846 if only {
8847 self.advance();
8848 }
8849 let table = self.expect_ident_like()?;
8850 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8851 // bare spelling; a bare identifier that is the SET keyword itself
8852 // is the clause, not an alias.
8853 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8854 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8855 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8856 // following JOIN a syntax error.
8857 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8858 let alias = if matches!(self.peek(), Token::As) {
8859 self.advance();
8860 Some(self.expect_ident_like()?)
8861 } else {
8862 match self.peek() {
8863 Token::Ident(s) | Token::QuotedIdent(s)
8864 if !s.eq_ignore_ascii_case("set") && !starts_join =>
8865 {
8866 let a = s.clone();
8867 self.advance();
8868 Some(a)
8869 }
8870 _ => None,
8871 }
8872 };
8873 // v7.39 (round 420) — MySQL's multi-table UPDATE:
8874 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
8875 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
8876 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8877 // The FIRST table is the mutation target and the rest are sources —
8878 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8879 // SPG already lowers onto correlated subqueries. So rewind, let
8880 // `parse_from_clause` read the whole list (it handles aliases, comma
8881 // lists, and every JOIN form), then peel the target off the front.
8882 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8883 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8884 {
8885 // NOTE: `advance()` destroys the tokens it returns
8886 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8887 // is NOT possible — the tail is read forward, once, through the
8888 // same grammar `parse_from_clause` uses after its primary.
8889 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8890 let mut joins = self.parse_from_joins(&target_qual)?;
8891 if joins.is_empty() {
8892 return Err(self.err(alloc::string::String::from(
8893 "multi-table UPDATE needs at least one source table",
8894 )));
8895 }
8896 let head = joins.remove(0);
8897 // A LEFT join keeps every target row (the unmatched ones see NULL
8898 // on the source side), so it must NOT get the EXISTS row filter
8899 // the inner / comma forms use.
8900 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8901 let src = FromClause {
8902 primary: head.table,
8903 joins,
8904 };
8905 (Some(src), head.on, outer)
8906 } else {
8907 (None, None, false)
8908 };
8909 self.expect_keyword_ident("set")?;
8910 let mut assignments = Vec::new();
8911 loop {
8912 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8913 // …)` — the parenthesized multi-assignment. Expressions
8914 // assign positionally; a subquery RHS clones per column
8915 // keeping only the Nth projection item.
8916 if matches!(self.peek(), Token::LParen) {
8917 self.advance();
8918 let mut cols = alloc::vec![self.expect_ident_like()?];
8919 while matches!(self.peek(), Token::Comma) {
8920 self.advance();
8921 cols.push(self.expect_ident_like()?);
8922 }
8923 if !matches!(self.peek(), Token::RParen) {
8924 return Err(self.err(format!(
8925 "expected ')' after SET column list, got {:?}",
8926 self.peek()
8927 )));
8928 }
8929 self.advance();
8930 if !matches!(self.peek(), Token::Eq) {
8931 return Err(self.err(format!(
8932 "expected `=` after SET column list, got {:?}",
8933 self.peek()
8934 )));
8935 }
8936 self.advance();
8937 if !matches!(self.peek(), Token::LParen) {
8938 return Err(self.err(format!(
8939 "expected '(' after SET (…) =, got {:?}",
8940 self.peek()
8941 )));
8942 }
8943 self.advance();
8944 if matches!(self.peek(), Token::Select) {
8945 let inner = match self.parse_select_stmt()? {
8946 Statement::Select(s) => s,
8947 other => {
8948 return Err(self.err(alloc::format!(
8949 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8950 )));
8951 }
8952 };
8953 if !matches!(self.peek(), Token::RParen) {
8954 return Err(self.err(format!(
8955 "expected ')' after SET subquery, got {:?}",
8956 self.peek()
8957 )));
8958 }
8959 self.advance();
8960 if inner.items.len() != cols.len() {
8961 return Err(self.err(alloc::format!(
8962 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8963 cols.len(),
8964 inner.items.len()
8965 )));
8966 }
8967 for (i, col) in cols.into_iter().enumerate() {
8968 let mut sub = inner.clone();
8969 sub.items = alloc::vec![sub.items[i].clone()];
8970 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8971 }
8972 } else {
8973 let mut exprs = alloc::vec![self.parse_expr(0)?];
8974 while matches!(self.peek(), Token::Comma) {
8975 self.advance();
8976 exprs.push(self.parse_expr(0)?);
8977 }
8978 if !matches!(self.peek(), Token::RParen) {
8979 return Err(self.err(format!(
8980 "expected ')' after SET row values, got {:?}",
8981 self.peek()
8982 )));
8983 }
8984 self.advance();
8985 if exprs.len() != cols.len() {
8986 return Err(self.err(alloc::format!(
8987 "SET (…) = (…) arity mismatch: {} columns, {} values",
8988 cols.len(),
8989 exprs.len()
8990 )));
8991 }
8992 for (col, e) in cols.into_iter().zip(exprs) {
8993 assignments.push((col, e));
8994 }
8995 }
8996 if matches!(self.peek(), Token::Comma) {
8997 self.advance();
8998 continue;
8999 }
9000 break;
9001 }
9002 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9003 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9004 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9005 // `public.` dump qualifiers), so the qualifier has to be read off
9006 // the token stream first — otherwise `SET b.v = 888` would write
9007 // to the TARGET table's `v` while naming a source table, a
9008 // silent-wrong. A qualifier naming a SOURCE table means a
9009 // multi-TARGET update — mutating two tables in one statement —
9010 // which SPG does not model, so it is refused loudly.
9011 let set_qual: Option<String> = if mysql_from.is_some()
9012 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9013 {
9014 match self.peek() {
9015 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9016 _ => None,
9017 }
9018 } else {
9019 None
9020 };
9021 let col = self.expect_ident_like()?;
9022 if let Some(q) = set_qual {
9023 let names_target = q.eq_ignore_ascii_case(&table)
9024 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9025 if !names_target {
9026 return Err(self.err(alloc::format!(
9027 "multi-table UPDATE can only assign to its first table \
9028 ({table}); `{q}.{col}` targets another table"
9029 )));
9030 }
9031 }
9032 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9033 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9034 // `__column_default` marker lowering just below). PG assigns to the
9035 // i-th (1-based) element, NULL-padding when i exceeds the length.
9036 if matches!(self.peek(), Token::LBracket) {
9037 self.advance();
9038 let index = self.parse_expr(0)?;
9039 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9040 // (and the open `arr[lo:]`), lowered to
9041 // `__array_assign_slice`. Only the single-subscript form
9042 // parsed before, so a slice assignment was a syntax error.
9043 let mut slice_hi: Option<Option<Expr>> = None;
9044 if matches!(self.peek(), Token::Colon) {
9045 self.advance();
9046 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9047 None
9048 } else {
9049 Some(self.parse_expr(0)?)
9050 });
9051 }
9052 if !matches!(self.peek(), Token::RBracket) {
9053 return Err(self.err(format!(
9054 "expected `]` after array subscript in UPDATE SET, got {:?}",
9055 self.peek()
9056 )));
9057 }
9058 self.advance();
9059 if !matches!(self.peek(), Token::Eq) {
9060 return Err(self.err(format!(
9061 "expected `=` after array subscript in UPDATE SET, got {:?}",
9062 self.peek()
9063 )));
9064 }
9065 self.advance();
9066 let value = self.parse_expr(0)?;
9067 // PG merges several subscript writes to the same column into one
9068 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9069 // assignment to `col` rather than each overwriting the original.
9070 let existing = assignments.iter().position(|(c, _)| c == &col);
9071 let base = match existing {
9072 Some(i) => assignments[i].1.clone(),
9073 None => Expr::Column(ColumnName {
9074 qualifier: None,
9075 name: col.clone(),
9076 }),
9077 };
9078 let assigned = match slice_hi {
9079 None => Expr::FunctionCall {
9080 name: "__array_assign".to_string(),
9081 args: alloc::vec![base, index, value],
9082 },
9083 Some(hi) => Expr::FunctionCall {
9084 name: "__array_assign_slice".to_string(),
9085 args: alloc::vec![
9086 base,
9087 index,
9088 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9089 value,
9090 ],
9091 },
9092 };
9093 match existing {
9094 Some(i) => assignments[i].1 = assigned,
9095 None => assignments.push((col, assigned)),
9096 }
9097 if matches!(self.peek(), Token::Comma) {
9098 self.advance();
9099 continue;
9100 }
9101 break;
9102 }
9103 if !matches!(self.peek(), Token::Eq) {
9104 return Err(self.err(format!(
9105 "expected `=` after column name in UPDATE SET, got {:?}",
9106 self.peek()
9107 )));
9108 }
9109 self.advance();
9110 // `SET col = DEFAULT` — the column's declared default;
9111 // rides out as a marker call the update executor
9112 // resolves against the schema.
9113 let value = if matches!(self.peek(), Token::Default) {
9114 self.advance();
9115 Expr::FunctionCall {
9116 name: "__column_default".to_string(),
9117 args: Vec::new(),
9118 }
9119 } else {
9120 self.parse_expr(0)?
9121 };
9122 assignments.push((col, value));
9123 if matches!(self.peek(), Token::Comma) {
9124 self.advance();
9125 continue;
9126 }
9127 break;
9128 }
9129 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9130 // update. Lowers onto the correlated-subquery machinery:
9131 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9132 // and each assignment that references a FROM-list table
9133 // wraps into a correlated scalar subquery
9134 // (SELECT expr FROM src WHERE cond). Equivalent for the
9135 // unique-join shape (the overwhelmingly common one); a
9136 // multi-match, which PG resolves by arbitrary pick,
9137 // surfaces as a scalar-subquery cardinality error instead
9138 // of a silent arbitrary result.
9139 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9140 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9141 // the SAME lowering below. Both spellings together is not legal in
9142 // either dialect.
9143 let from_clause = if let Some(fc) = mysql_from {
9144 if matches!(self.peek(), Token::From) {
9145 return Err(self.err(alloc::string::String::from(
9146 "multi-table UPDATE already names its sources; drop the FROM clause",
9147 )));
9148 }
9149 Some(fc)
9150 } else if matches!(self.peek(), Token::From) {
9151 self.advance();
9152 Some(self.parse_from_clause()?)
9153 } else {
9154 None
9155 };
9156 let where_ = if matches!(self.peek(), Token::Where) {
9157 self.advance();
9158 Some(self.parse_expr(0)?)
9159 } else {
9160 None
9161 };
9162 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9163 // and the TARGET-row filter are NOT the same predicate once a LEFT
9164 // join is involved:
9165 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9166 // one conjunction, and the whole thing filters target rows via
9167 // EXISTS.
9168 // * LEFT join: only the ON predicate belongs inside the source
9169 // subquery. The WHERE still filters TARGET rows (with source
9170 // columns read through the correlated subquery, which yields NULL
9171 // for an unmatched row — exactly LEFT-join semantics).
9172 // Round 420 folded ON into WHERE unconditionally and then dropped the
9173 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9174 // WHERE a.id > 1` updated EVERY row.
9175 let sub_where = match (mysql_on.clone(), where_.clone()) {
9176 _ if mysql_outer => mysql_on.clone(),
9177 (Some(on), Some(w)) => Some(Expr::Binary {
9178 lhs: Box::new(on),
9179 op: crate::ast::BinOp::And,
9180 rhs: Box::new(w),
9181 }),
9182 (Some(on), None) => Some(on),
9183 (None, w) => w,
9184 };
9185 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9186 // has no such clause on UPDATE, so this is accepted only under the
9187 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9188 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9189 let mut returning = self.parse_optional_returning()?;
9190 // v7.39 (round 533) — kept for the engine, which can resolve the
9191 // UNQUALIFIED leaves this lowering has to leave alone.
9192 let from_sources = from_clause.as_ref().map(|fc| {
9193 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9194 from: fc.clone(),
9195 sub_where: sub_where.clone(),
9196 })
9197 });
9198 let (assignments, where_) = if let Some(fc) = from_clause {
9199 let names: Vec<String> = core::iter::once(&fc.primary)
9200 .chain(fc.joins.iter().map(|j| &j.table))
9201 .flat_map(|t| {
9202 t.alias
9203 .clone()
9204 .into_iter()
9205 .chain(core::iter::once(t.name.clone()))
9206 })
9207 .collect();
9208 let refs_list = |e: &Expr| -> bool {
9209 fn walk(e: &Expr, names: &[String]) -> bool {
9210 match e {
9211 Expr::Column(c) => c
9212 .qualifier
9213 .as_deref()
9214 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9215 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9216 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9217 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9218 Expr::Case {
9219 operand,
9220 branches,
9221 else_branch,
9222 } => {
9223 operand.as_deref().is_some_and(|o| walk(o, names))
9224 || branches
9225 .iter()
9226 .any(|(w, t)| walk(w, names) || walk(t, names))
9227 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9228 }
9229 _ => false,
9230 }
9231 }
9232 walk(e, &names)
9233 };
9234 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9235 locking: None,
9236 ctes: Vec::new(),
9237 distinct: false,
9238 distinct_on: Vec::new(),
9239 items,
9240 from: Some(fc.clone()),
9241 where_: sub_where.clone(),
9242 group_by: None,
9243 group_by_all: false,
9244 having: None,
9245 unions: Vec::new(),
9246 order_by: Vec::new(),
9247 limit: None,
9248 offset: None,
9249 limit_with_ties: false,
9250 window_check_exprs: Vec::new(),
9251 };
9252 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9253 // assignment RHS with a correlated scalar subquery, instead of
9254 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9255 // column reference (`SET v = v + u.bonus`, where `v` is the target
9256 // table's column) inside a subquery whose FROM only has the source
9257 // table, so the unqualified `v` resolved against the source and
9258 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9259 // context — where they belong — fixes it; only the source columns
9260 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9261 // compound variants the leaf-walk doesn't decompose.
9262 let make_subq = |inner: Expr| {
9263 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9264 expr: inner,
9265 alias: None,
9266 }])))
9267 };
9268 let assignments = assignments
9269 .into_iter()
9270 .map(|(col, mut expr)| {
9271 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9272 (col, expr)
9273 })
9274 .collect();
9275 let exists = Expr::Exists {
9276 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9277 expr: Expr::Literal(Literal::Integer(1)),
9278 alias: None,
9279 }])),
9280 negated: false,
9281 };
9282 // v7.39 (round 241) — RETURNING may reference the FROM-list
9283 // tables too (`RETURNING emp.id, dept.name`); the same
9284 // leaf-to-correlated-subquery lowering the assignments get.
9285 // Without it the qualifier died at eval with "unknown table
9286 // qualifier". (RETURNING was parsed before this block — the
9287 // lowering is a pure AST transformation.)
9288 if let Some(items) = returning.as_mut() {
9289 for item in items.iter_mut() {
9290 if let SelectItem::Expr { expr, .. } = item {
9291 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9292 }
9293 }
9294 }
9295 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9296 // EVERY matching target row: it gets no EXISTS filter, but the
9297 // caller's WHERE still applies, with source columns read through
9298 // the correlated subquery (NULL when unmatched — LEFT-join
9299 // semantics). `sub_where` above already excluded the WHERE from
9300 // the source subquery for this case.
9301 if mysql_outer {
9302 let mut outer = where_;
9303 if let Some(w) = outer.as_mut() {
9304 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9305 }
9306 (assignments, outer)
9307 } else {
9308 (assignments, Some(exists))
9309 }
9310 } else {
9311 (assignments, where_)
9312 };
9313 Ok(Statement::Update(crate::ast::UpdateStatement {
9314 ctes: Vec::new(),
9315 table,
9316 only,
9317 alias,
9318 assignments,
9319 from_sources,
9320 where_,
9321 order_limit: update_order_limit,
9322 returning,
9323 }))
9324 }
9325
9326 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9327 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9328 /// clause and its meaning are identical, so both call this rather than
9329 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9330 /// legal. PG has no such clause on either statement, so it is read only
9331 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9332 /// errors.
9333 ///
9334 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9335 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9336 /// stack in round 430.
9337 #[inline(never)]
9338 fn parse_mysql_dml_order_limit(
9339 &mut self,
9340 what: &str,
9341 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9342 if !self.mysql_dialect {
9343 return Ok(None);
9344 }
9345 let order_by = self.parse_order_by_keys()?;
9346 let limit = if matches!(self.peek(), Token::Limit) {
9347 self.advance();
9348 let tok = self.advance();
9349 let Token::Integer(n) = tok else {
9350 return Err(self.err(alloc::format!(
9351 "expected integer after {what} LIMIT, got {tok:?}"
9352 )));
9353 };
9354 // MySQL rejects the `LIMIT offset, count` form here — only a
9355 // single row count is legal on a DML statement.
9356 if matches!(self.peek(), Token::Comma) {
9357 return Err(self.err(alloc::format!(
9358 "{what} LIMIT takes a row count, not an offset"
9359 )));
9360 }
9361 let n = u32::try_from(n)
9362 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9363 Some(n)
9364 } else {
9365 None
9366 };
9367 if order_by.is_empty() && limit.is_none() {
9368 return Ok(None);
9369 }
9370 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9371 order_by,
9372 limit,
9373 })))
9374 }
9375
9376 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9377 /// the leading `DELETE` ident.
9378 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9379 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9380 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9381 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9382 // parse here; it reaches the existing USING path with the target
9383 // repeated in the list, which the source-list peel below handles.)
9384 // More than one name is a multi-TARGET delete, which SPG does not
9385 // model; it is refused rather than half-applied.
9386 let mysql_pre_target: Option<String> =
9387 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9388 let first = self.expect_ident_like()?;
9389 if matches!(self.peek(), Token::Comma) {
9390 return Err(self.err(alloc::format!(
9391 "multi-table DELETE can only delete from one table; \
9392 `DELETE {first}, …` names several"
9393 )));
9394 }
9395 Some(first)
9396 } else {
9397 None
9398 };
9399 if !matches!(self.peek(), Token::From) {
9400 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9401 }
9402 self.advance();
9403 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9404 // lookahead as the UPDATE spelling.
9405 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9406 if s.eq_ignore_ascii_case("only"))
9407 && matches!(
9408 self.tokens.get(self.pos + 1),
9409 Some(Token::Ident(_) | Token::QuotedIdent(_))
9410 );
9411 if only {
9412 self.advance();
9413 }
9414 let table = self.expect_ident_like()?;
9415 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9416 // spelling must not swallow the clause keywords that can follow
9417 // the target.
9418 let alias = if matches!(self.peek(), Token::As) {
9419 self.advance();
9420 Some(self.expect_ident_like()?)
9421 } else {
9422 match self.peek() {
9423 Token::Ident(s) | Token::QuotedIdent(s)
9424 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9425 {
9426 let a = s.clone();
9427 self.advance();
9428 Some(a)
9429 }
9430 _ => None,
9431 }
9432 };
9433 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9434 // through the SAME join grammar the FROM clause uses (see the
9435 // `advance()`-destroys-tokens note on `parse_from_joins`).
9436 let mut mysql_on: Option<Expr> = None;
9437 let mut mysql_outer = false;
9438 let mysql_using = if mysql_pre_target.is_some()
9439 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9440 {
9441 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9442 let mut joins = self.parse_from_joins(&target_qual)?;
9443 if joins.is_empty() {
9444 return Err(self.err(alloc::string::String::from(
9445 "multi-table DELETE needs at least one source table",
9446 )));
9447 }
9448 let head = joins.remove(0);
9449 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9450 mysql_on = head.on;
9451 Some(FromClause {
9452 primary: head.table,
9453 joins,
9454 })
9455 } else {
9456 None
9457 };
9458 // The pre-FROM target must be the table the FROM names (or its
9459 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9460 // is not the scan target.
9461 if let Some(t) = &mysql_pre_target {
9462 let names_target = t.eq_ignore_ascii_case(&table)
9463 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9464 if !names_target {
9465 return Err(self.err(alloc::format!(
9466 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9467 )));
9468 }
9469 }
9470 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9471 // delete. Same lowering as UPDATE … FROM: the WHERE
9472 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9473 // target row by the correlated machinery.
9474 let using_clause = if let Some(fc) = mysql_using {
9475 Some(fc)
9476 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9477 self.advance();
9478 let mut fc = self.parse_from_clause()?;
9479 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9480 // repeats the TARGET as the first USING entry (PG's spelling
9481 // lists only the extra sources). Peel it so the source subquery
9482 // does not re-scan — and shadow — the target table.
9483 let primary_is_target =
9484 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9485 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9486 let head = fc.joins.remove(0);
9487 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9488 mysql_on = head.on;
9489 fc = FromClause {
9490 primary: head.table,
9491 joins: fc.joins,
9492 };
9493 }
9494 Some(fc)
9495 } else {
9496 None
9497 };
9498 let where_ = if matches!(self.peek(), Token::Where) {
9499 self.advance();
9500 Some(self.parse_expr(0)?)
9501 } else {
9502 None
9503 };
9504 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9505 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9506 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9507 let mut returning = self.parse_optional_returning()?;
9508 let where_ = if let Some(fc) = using_clause {
9509 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9510 // a USING-table reference in RETURNING becomes a correlated
9511 // scalar subquery over the USING list.
9512 let names: Vec<String> = core::iter::once(&fc.primary)
9513 .chain(fc.joins.iter().map(|j| &j.table))
9514 .flat_map(|t| {
9515 t.alias
9516 .clone()
9517 .into_iter()
9518 .chain(core::iter::once(t.name.clone()))
9519 })
9520 .collect();
9521 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9522 // join filters the SOURCE subquery on the ON predicate alone and
9523 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9524 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9525 // rows); every other form folds ON and WHERE into one EXISTS.
9526 let sub_where = match (mysql_on.clone(), where_.clone()) {
9527 _ if mysql_outer => mysql_on.clone(),
9528 (Some(on), Some(w)) => Some(Expr::Binary {
9529 lhs: Box::new(on),
9530 op: crate::ast::BinOp::And,
9531 rhs: Box::new(w),
9532 }),
9533 (Some(on), None) => Some(on),
9534 (None, w) => w,
9535 };
9536 let exists_where = sub_where.clone();
9537 let sub_fc = fc.clone();
9538 let make_subq = move |leaf: Expr| -> Expr {
9539 Expr::ScalarSubquery(Box::new(SelectStatement {
9540 locking: None,
9541 ctes: Vec::new(),
9542 distinct: false,
9543 distinct_on: Vec::new(),
9544 items: alloc::vec![SelectItem::Expr {
9545 expr: leaf,
9546 alias: None,
9547 }],
9548 from: Some(sub_fc.clone()),
9549 where_: sub_where.clone(),
9550 group_by: None,
9551 group_by_all: false,
9552 having: None,
9553 unions: Vec::new(),
9554 order_by: Vec::new(),
9555 limit: None,
9556 offset: None,
9557 limit_with_ties: false,
9558 window_check_exprs: Vec::new(),
9559 }))
9560 };
9561 let refs = |e: &Expr| expr_refs_tables(e, &names);
9562 if let Some(items) = returning.as_mut() {
9563 for item in items.iter_mut() {
9564 if let SelectItem::Expr { expr, .. } = item {
9565 wrap_from_leaves(expr, &names, &make_subq, &refs);
9566 }
9567 }
9568 }
9569 // A LEFT join deletes the target rows the WHERE selects, reading
9570 // source columns through the correlated subquery (NULL when
9571 // unmatched); no EXISTS row filter.
9572 if mysql_outer {
9573 let mut outer = where_;
9574 if let Some(w) = outer.as_mut() {
9575 wrap_from_leaves(w, &names, &make_subq, &refs);
9576 }
9577 outer
9578 } else {
9579 Some(Expr::Exists {
9580 subquery: Box::new(SelectStatement {
9581 locking: None,
9582 ctes: Vec::new(),
9583 distinct: false,
9584 distinct_on: Vec::new(),
9585 items: alloc::vec![SelectItem::Expr {
9586 expr: Expr::Literal(Literal::Integer(1)),
9587 alias: None,
9588 }],
9589 from: Some(fc),
9590 where_: exists_where,
9591 group_by: None,
9592 group_by_all: false,
9593 having: None,
9594 unions: Vec::new(),
9595 order_by: Vec::new(),
9596 limit: None,
9597 offset: None,
9598 limit_with_ties: false,
9599 window_check_exprs: Vec::new(),
9600 }),
9601 negated: false,
9602 })
9603 }
9604 } else {
9605 where_
9606 };
9607 Ok(Statement::Delete(crate::ast::DeleteStatement {
9608 ctes: Vec::new(),
9609 table,
9610 only,
9611 alias,
9612 where_,
9613 order_limit: delete_order_limit,
9614 returning,
9615 }))
9616 }
9617
9618 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9619 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9620 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9621 /// keyword. v7.17 surface:
9622 /// * source: table reference (subquery source is a follow-up)
9623 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9624 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9625 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9626 /// order
9627 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9628 // INTO
9629 let is_into_kw = matches!(self.peek(), Token::Into)
9630 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9631 if !is_into_kw {
9632 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9633 }
9634 self.advance();
9635 let target = self.expect_ident_like()?;
9636 // Optional alias — bare ident before USING.
9637 let target_alias = match self.peek() {
9638 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9639 Some(self.expect_ident_like()?)
9640 }
9641 _ => None,
9642 };
9643 // USING
9644 let is_using_kw = matches!(
9645 self.peek(),
9646 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9647 );
9648 if !is_using_kw {
9649 return Err(self.err(format!(
9650 "expected USING after MERGE INTO target, got {:?}",
9651 self.peek()
9652 )));
9653 }
9654 self.advance();
9655 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9656 // <table> [alias]`. PG requires an alias after a subquery source.
9657 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9658 self.advance(); // (
9659 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9660 // constant-SELECT lowering the derived-table parser uses
9661 // (PG deletes through this form; it was a parse error).
9662 let inner = if matches!(self.peek(), Token::Values) {
9663 self.advance(); // VALUES
9664 Statement::Select(self.parse_values_rows_body()?)
9665 } else {
9666 self.parse_select_stmt()?
9667 };
9668 match self.advance() {
9669 Token::RParen => {}
9670 other => {
9671 return Err(self.err(format!(
9672 "expected ')' after MERGE USING subquery, got {other:?}"
9673 )));
9674 }
9675 }
9676 let Statement::Select(sub) = inner else {
9677 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9678 };
9679 (String::new(), Some(Box::new(sub)))
9680 } else {
9681 (self.expect_ident_like()?, None)
9682 };
9683 let source_alias = match self.peek() {
9684 Token::Ident(s) | Token::QuotedIdent(s)
9685 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9686 {
9687 Some(self.expect_ident_like()?)
9688 }
9689 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9690 self.advance(); // AS
9691 Some(self.expect_ident_like()?)
9692 }
9693 _ => None,
9694 };
9695 // v7.39 (round 768, F31-D5) — optional positional column-alias
9696 // list after the source alias (`s(id, v)`).
9697 let mut source_column_aliases: Vec<String> = Vec::new();
9698 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9699 self.advance();
9700 loop {
9701 source_column_aliases.push(self.expect_ident_like()?);
9702 match self.peek() {
9703 Token::Comma => {
9704 self.advance();
9705 }
9706 Token::RParen => {
9707 self.advance();
9708 break;
9709 }
9710 other => {
9711 return Err(self.err(format!(
9712 "expected ',' or ')' in MERGE source column list, got {other:?}"
9713 )));
9714 }
9715 }
9716 }
9717 }
9718 if source_select.is_some() && source_alias.is_none() {
9719 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9720 }
9721 // ON
9722 if !matches!(self.peek(), Token::On) {
9723 return Err(self.err(format!(
9724 "expected ON after MERGE … USING source, got {:?}",
9725 self.peek()
9726 )));
9727 }
9728 self.advance();
9729 let on = self.parse_expr(0)?;
9730 // One or more WHEN clauses.
9731 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9732 loop {
9733 let is_when_kw = matches!(
9734 self.peek(),
9735 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9736 );
9737 if !is_when_kw {
9738 break;
9739 }
9740 self.advance(); // WHEN
9741 // [NOT] MATCHED
9742 let matched = if matches!(self.peek(), Token::Not) {
9743 self.advance();
9744 crate::ast::MergeMatched::NotMatched
9745 } else {
9746 crate::ast::MergeMatched::Matched
9747 };
9748 let is_matched_kw = matches!(
9749 self.peek(),
9750 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9751 );
9752 if !is_matched_kw {
9753 return Err(self.err(format!(
9754 "expected MATCHED in WHEN clause, got {:?}",
9755 self.peek()
9756 )));
9757 }
9758 self.advance();
9759 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9760 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9761 // to fire for target rows no source row matches.
9762 let mut matched = matched;
9763 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9764 self.advance();
9765 match self.peek() {
9766 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9767 self.advance();
9768 matched = crate::ast::MergeMatched::NotMatchedBySource;
9769 }
9770 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9771 self.advance();
9772 }
9773 other => {
9774 return Err(self.err(format!(
9775 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9776 )));
9777 }
9778 }
9779 }
9780 // Optional AND <expr>
9781 let condition = if matches!(self.peek(), Token::And) {
9782 self.advance();
9783 Some(self.parse_expr(0)?)
9784 } else {
9785 None
9786 };
9787 // THEN
9788 let is_then_kw = matches!(
9789 self.peek(),
9790 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9791 );
9792 if !is_then_kw {
9793 return Err(self.err(format!(
9794 "expected THEN in WHEN clause, got {:?}",
9795 self.peek()
9796 )));
9797 }
9798 self.advance();
9799 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9800 let action = match self.peek().clone() {
9801 Token::Insert => {
9802 self.advance();
9803 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9804 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9805 // VALUES (…)` omits it and fills every column in declaration
9806 // order. PG accepts this; SPG used to require the list.
9807 let mut columns: Vec<String> = Vec::new();
9808 if matches!(self.peek(), Token::LParen) {
9809 self.advance();
9810 loop {
9811 columns.push(self.expect_ident_like()?);
9812 if matches!(self.peek(), Token::Comma) {
9813 self.advance();
9814 continue;
9815 }
9816 break;
9817 }
9818 if !matches!(self.peek(), Token::RParen) {
9819 return Err(self.err(format!(
9820 "expected ')' after INSERT column list, got {:?}",
9821 self.peek()
9822 )));
9823 }
9824 self.advance();
9825 }
9826 // VALUES (...)
9827 if !matches!(self.peek(), Token::Values) {
9828 return Err(self.err(format!(
9829 "expected VALUES in MERGE INSERT, got {:?}",
9830 self.peek()
9831 )));
9832 }
9833 self.advance();
9834 if !matches!(self.peek(), Token::LParen) {
9835 return Err(self.err(format!(
9836 "expected '(' after VALUES in MERGE INSERT, got {:?}",
9837 self.peek()
9838 )));
9839 }
9840 self.advance();
9841 let mut values: Vec<crate::ast::Expr> = Vec::new();
9842 loop {
9843 values.push(self.parse_expr(0)?);
9844 if matches!(self.peek(), Token::Comma) {
9845 self.advance();
9846 continue;
9847 }
9848 break;
9849 }
9850 if !matches!(self.peek(), Token::RParen) {
9851 return Err(self.err(format!(
9852 "expected ')' after MERGE INSERT values, got {:?}",
9853 self.peek()
9854 )));
9855 }
9856 self.advance();
9857 // Empty column list = positional into every column, so the
9858 // count is checked against the table arity at execution.
9859 if !columns.is_empty() && columns.len() != values.len() {
9860 return Err(self.err(format!(
9861 "MERGE INSERT column count ({}) ≠ value count ({})",
9862 columns.len(),
9863 values.len()
9864 )));
9865 }
9866 crate::ast::MergeAction::Insert { columns, values }
9867 }
9868 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9869 self.advance();
9870 // SET
9871 let is_set_kw = matches!(
9872 self.peek(),
9873 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9874 );
9875 if !is_set_kw {
9876 return Err(self.err(format!(
9877 "expected SET after UPDATE in MERGE, got {:?}",
9878 self.peek()
9879 )));
9880 }
9881 self.advance();
9882 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9883 loop {
9884 let col = self.expect_ident_like()?;
9885 if !matches!(self.peek(), Token::Eq) {
9886 return Err(self.err(format!(
9887 "expected '=' in MERGE UPDATE assignment, got {:?}",
9888 self.peek()
9889 )));
9890 }
9891 self.advance();
9892 let expr = self.parse_expr(0)?;
9893 assignments.push((col, expr));
9894 if matches!(self.peek(), Token::Comma) {
9895 self.advance();
9896 continue;
9897 }
9898 break;
9899 }
9900 crate::ast::MergeAction::Update { assignments }
9901 }
9902 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9903 self.advance();
9904 crate::ast::MergeAction::Delete
9905 }
9906 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9907 self.advance();
9908 let is_nothing_kw = matches!(
9909 self.peek(),
9910 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9911 );
9912 if !is_nothing_kw {
9913 return Err(self.err(format!(
9914 "expected NOTHING after DO in MERGE clause, got {:?}",
9915 self.peek()
9916 )));
9917 }
9918 self.advance();
9919 crate::ast::MergeAction::DoNothing
9920 }
9921 other => {
9922 return Err(self.err(format!(
9923 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9924 )));
9925 }
9926 };
9927 // PG's grammar simply has no INSERT production under BY SOURCE
9928 // (a target row already exists there) — same syntax error.
9929 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9930 && matches!(action, crate::ast::MergeAction::Insert { .. })
9931 {
9932 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9933 }
9934 clauses.push(crate::ast::MergeWhenClause {
9935 matched,
9936 condition,
9937 action,
9938 });
9939 }
9940 if clauses.is_empty() {
9941 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9942 }
9943 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9944 // unconditional (no `AND`) WHEN of the same match kind: it could
9945 // never fire. Check per match kind in clause order.
9946 let mut seen_unconditional_matched = false;
9947 let mut seen_unconditional_not_matched = false;
9948 let mut seen_unconditional_by_source = false;
9949 for c in &clauses {
9950 let seen = match c.matched {
9951 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9952 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9953 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9954 };
9955 if *seen {
9956 return Err(self.err(String::from(
9957 "unreachable WHEN clause specified after unconditional WHEN clause",
9958 )));
9959 }
9960 if c.condition.is_none() {
9961 *seen = true;
9962 }
9963 }
9964 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9965 let returning = self.parse_optional_returning()?;
9966 Ok(Statement::Merge(crate::ast::MergeStatement {
9967 // Attached by `parse_with_cte_then_select` when the MERGE
9968 // heads a WITH clause (round 149).
9969 ctes: Vec::new(),
9970 target,
9971 target_alias,
9972 source,
9973 source_alias,
9974 source_select,
9975 source_column_aliases,
9976 on,
9977 clauses,
9978 returning,
9979 }))
9980 }
9981
9982 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9983 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9984 /// as SELECT, so `RETURNING *`, `RETURNING col`,
9985 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9986 fn parse_optional_returning(
9987 &mut self,
9988 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
9989 let is_returning_kw = matches!(
9990 self.peek(),
9991 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
9992 );
9993 if !is_returning_kw {
9994 return Ok(None);
9995 }
9996 self.advance();
9997 let mut items = Vec::new();
9998 loop {
9999 items.push(self.parse_select_item()?);
10000 if matches!(self.peek(), Token::Comma) {
10001 self.advance();
10002 continue;
10003 }
10004 break;
10005 }
10006 Ok(Some(items))
10007 }
10008
10009 /// v6.0.4 — parse the tail of an ALTER statement after the
10010 /// leading `ALTER` keyword has been consumed. Only one form is
10011 /// supported in v6.0.4:
10012 ///
10013 /// ```text
10014 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10015 /// ```
10016 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10017 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10018 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10019 // exclusion) is accepted by stripping the `ONLY` keyword
10020 // before the table parse.
10021 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10022 // and the long PG-dump tail are accepted as no-ops.
10023 match self.advance() {
10024 Token::Index => {}
10025 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10026 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10027 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10028 Token::Table => {
10029 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10030 self.advance();
10031 }
10032 return self.parse_alter_table_after_keyword();
10033 }
10034 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10035 return self.parse_alter_policy_after_keyword();
10036 }
10037 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10038 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10039 self.advance();
10040 }
10041 return self.parse_alter_table_after_keyword();
10042 }
10043 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10044 // of the silent-noop tail.
10045 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10046 return self.parse_alter_sequence_after_keyword();
10047 }
10048 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10049 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10050 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10051 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10052 // NB: the match arm consumed `TYPE` via self.advance(); the
10053 // cursor is now at the type name — do NOT advance again.
10054 let type_name = self.expect_ident_like()?;
10055 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10056 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10057 if is_add_value {
10058 self.advance(); // ADD
10059 self.advance(); // VALUE
10060 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10061 // IF/EXISTS as identifiers.
10062 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10063 {
10064 let n1 = self.tokens.get(self.pos + 1);
10065 let n2 = self.tokens.get(self.pos + 2);
10066 if matches!(n1, Some(Token::Not))
10067 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10068 {
10069 self.advance();
10070 self.advance();
10071 self.advance();
10072 true
10073 } else {
10074 false
10075 }
10076 } else {
10077 false
10078 };
10079 let label = self.expect_string_literal()?;
10080 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10081 {
10082 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10083 self.advance();
10084 let anchor = self.expect_string_literal()?;
10085 Some((is_before, anchor))
10086 } else {
10087 None
10088 };
10089 return Ok(Statement::AlterTypeAddValue {
10090 type_name,
10091 label,
10092 if_not_exists,
10093 position,
10094 });
10095 }
10096 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10097 // Used to fall into the no-op tail below: accepted, silently
10098 // ignored. `RENAME TO <newtype>` keeps falling through.
10099 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10100 && matches!(
10101 self.tokens.get(self.pos + 1),
10102 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10103 )
10104 {
10105 self.advance(); // RENAME
10106 self.advance(); // VALUE
10107 let old = self.expect_string_literal()?;
10108 if matches!(self.peek(), Token::To) {
10109 self.advance();
10110 } else {
10111 self.expect_keyword_ident("to")?;
10112 }
10113 let new = self.expect_string_literal()?;
10114 return Ok(Statement::AlterTypeRenameValue {
10115 type_name,
10116 old,
10117 new,
10118 });
10119 }
10120 // Other ALTER TYPE forms — the ACTION stays a no-op
10121 // (pg_dump tail), but v7.39 (round 708) the NAME is
10122 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10123 // success for a type that does not exist.
10124 self.consume_until_statement_boundary();
10125 return Ok(Statement::ValidateOnly {
10126 kind: crate::ast::ValidateOnlyKind::TypeName,
10127 names: alloc::vec![type_name],
10128 });
10129 }
10130 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10131 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10132 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10133 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10134 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10135 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10136 // pg_dump no-op list below: every form used to report success
10137 // and change nothing, which is worse than refusing outright
10138 // (a migration dropping a constraint kept rejecting data).
10139 // NOTE: the enclosing `match self.advance()` already consumed
10140 // the DOMAIN keyword, so the name is next.
10141 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10142 return self.parse_alter_domain_after_keyword();
10143 }
10144 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10145 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10146 // used to fall into the pg_dump no-op tail below, so a DBA
10147 // setting a per-role default was told it worked and nothing
10148 // happened. Intercepted here, BEFORE that tail.
10149 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10150 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10151 // interception below exists: swallowed with the no-op tail, an
10152 // unknown parameter name was ACCEPTED where PG18 answers
10153 // `unrecognized configuration parameter`. SPG applies nothing
10154 // either way — there is no postgresql.auto.conf — but it now
10155 // says so about a name it does not know.
10156 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10157 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10158 // already consumed here. An extra advance eats the SET and
10159 // the parameter name is never seen — which is exactly the
10160 // bug a panic in this branch disproved: the branch WAS on
10161 // the path, the reading of it was wrong.
10162 let mut parameter = None;
10163 // SET <name> … | RESET <name> | RESET ALL
10164 if matches!(self.peek(), Token::Ident(k)
10165 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10166 {
10167 self.advance();
10168 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10169 && !n.eq_ignore_ascii_case("all")
10170 {
10171 self.advance();
10172 // A dotted GUC (`plpgsql.check_asserts`) is two
10173 // tokens; keep the whole name.
10174 let mut full = n;
10175 while matches!(self.peek(), Token::Dot) {
10176 self.advance();
10177 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10178 full.push('.');
10179 full.push_str(&t);
10180 }
10181 }
10182 parameter = Some(full);
10183 }
10184 }
10185 self.consume_until_statement_boundary();
10186 return Ok(Statement::AlterSystem { parameter });
10187 }
10188 Token::Ident(s) | Token::QuotedIdent(s)
10189 if matches!(
10190 s.to_ascii_lowercase().as_str(),
10191 "role" | "user" | "database"
10192 ) && self.peeks_db_role_setting() =>
10193 {
10194 let is_database = s.eq_ignore_ascii_case("database");
10195 return self.parse_db_role_setting(is_database);
10196 }
10197 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10198 // (the non-SET forms; SET/RESET took the branch above). The
10199 // attributes still no-op — recorded, and the ignored PASSWORD
10200 // is ledgered as its own follow-up — but the ROLE is validated:
10201 // any name was accepted for a role that does not exist.
10202 Token::Ident(s) | Token::QuotedIdent(s)
10203 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10204 {
10205 // NB: the enclosing `match self.advance()` already consumed
10206 // ROLE/USER — the round-695 trap, hit again in this round's
10207 // first draft (the name was eaten and WITH parsed as the
10208 // role). The cursor is at the name.
10209 let name = self.expect_ident_or_string()?;
10210 // v7.39 (round 750) — scan the attribute tail for
10211 // PASSWORD. Everything else stays a recorded no-op, but
10212 // a dropped credential rotation is a SECURITY bug:
10213 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10214 // changed nothing, so the old password kept working.
10215 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10216 // NULL` clears the credential.
10217 let mut password: Option<Option<String>> = None;
10218 loop {
10219 match self.peek() {
10220 Token::Semicolon | Token::Eof => break,
10221 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10222 self.advance();
10223 match self.advance() {
10224 Token::String(p) => password = Some(Some(p)),
10225 Token::Null => password = Some(None),
10226 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10227 password = Some(None);
10228 }
10229 other => {
10230 return Err(self.err(alloc::format!(
10231 "expected password string or NULL after PASSWORD, got {other:?}"
10232 )));
10233 }
10234 }
10235 }
10236 _ => {
10237 self.advance();
10238 }
10239 }
10240 }
10241 if name.eq_ignore_ascii_case("all") {
10242 // `ALTER ROLE ALL …` names every role; nothing to check.
10243 return Ok(Statement::Empty);
10244 }
10245 if let Some(pw) = password {
10246 return Ok(Statement::AlterRolePassword { name, password: pw });
10247 }
10248 return Ok(Statement::ValidateOnly {
10249 kind: crate::ast::ValidateOnlyKind::RoleName,
10250 names: alloc::vec![name],
10251 });
10252 }
10253 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10254 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10255 // list far enough to validate the NAME; the actions still no-op.
10256 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10257 // models none of them and their dumps are rare.)
10258 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10259 let name = self.expect_ident_or_string()?;
10260 self.consume_until_statement_boundary();
10261 return Ok(Statement::ValidateOnly {
10262 kind: crate::ast::ValidateOnlyKind::CollationName,
10263 names: alloc::vec![name],
10264 });
10265 }
10266 Token::Ident(s) | Token::QuotedIdent(s)
10267 if s.eq_ignore_ascii_case("text")
10268 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10269 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10270 {
10271 self.advance(); // SEARCH
10272 self.advance(); // CONFIGURATION
10273 let name = self.expect_ident_like()?;
10274 self.consume_until_statement_boundary();
10275 return Ok(Statement::ValidateOnly {
10276 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10277 names: alloc::vec![name],
10278 });
10279 }
10280 Token::Ident(s) | Token::QuotedIdent(s)
10281 if s.eq_ignore_ascii_case("event")
10282 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10283 {
10284 self.advance(); // TRIGGER
10285 let name = self.expect_ident_like()?;
10286 self.consume_until_statement_boundary();
10287 return Ok(Statement::ValidateOnly {
10288 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10289 names: alloc::vec![name],
10290 });
10291 }
10292 Token::Ident(s) | Token::QuotedIdent(s)
10293 if s.eq_ignore_ascii_case("large")
10294 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10295 {
10296 self.advance(); // OBJECT
10297 let oid = match self.advance() {
10298 Token::Integer(n) => alloc::format!("{n}"),
10299 other => {
10300 return Err(
10301 self.err(alloc::format!("expected large object oid, got {other:?}"))
10302 );
10303 }
10304 };
10305 self.consume_until_statement_boundary();
10306 return Ok(Statement::ValidateOnly {
10307 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10308 names: alloc::vec![oid],
10309 });
10310 }
10311 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10312 // argument-list parse as DROP AGGREGATE (round 707); the
10313 // action no-ops, the existence check is real.
10314 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10315 // Same round-695 trap as above: AGGREGATE is already
10316 // consumed; the cursor is at the name.
10317 let name = self.expect_ident_like()?;
10318 let mut names = alloc::vec![name];
10319 if matches!(self.peek(), Token::LParen) {
10320 self.advance();
10321 loop {
10322 match self.peek().clone() {
10323 Token::RParen => {
10324 self.advance();
10325 break;
10326 }
10327 Token::Star => {
10328 self.advance();
10329 names.push(String::from("*"));
10330 }
10331 Token::Comma => {
10332 self.advance();
10333 }
10334 _ => {
10335 let mut t = self.expect_ident_like()?;
10336 while let Token::Ident(nx) = self.peek() {
10337 let nx = nx.clone();
10338 self.advance();
10339 t.push(' ');
10340 t.push_str(&nx);
10341 }
10342 names.push(t);
10343 }
10344 }
10345 }
10346 }
10347 self.consume_until_statement_boundary();
10348 return Ok(Statement::ValidateOnly {
10349 kind: crate::ast::ValidateOnlyKind::AggregateName,
10350 names,
10351 });
10352 }
10353 Token::Ident(s) | Token::QuotedIdent(s)
10354 if matches!(
10355 s.to_ascii_lowercase().as_str(),
10356 "view"
10357 | "function"
10358 | "database"
10359 | "schema"
10360 | "owner"
10361 | "default"
10362 | "extension"
10363 | "materialized"
10364 | "publication"
10365 | "subscription"
10366 // v7.37.17 (17.6 siblings) — additional ALTER
10367 // targets pg_dump / pg_dumpall / operator DB
10368 // migration scripts commonly emit. SPG has
10369 // no matching machinery for any of these; the
10370 // parser accepts + Empty-returns so pg_dump
10371 // tail statements don't stall.
10372 | "tablespace"
10373 | "language"
10374 | "operator"
10375 | "conversion"
10376 | "statistics"
10377 | "server"
10378 | "foreign"
10379 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10380 // / TEMPLATE (CONFIGURATION intercepted above).
10381 | "text"
10382 ) =>
10383 {
10384 self.consume_until_statement_boundary();
10385 return Ok(Statement::Empty);
10386 }
10387 other => {
10388 return Err(self.err(format!(
10389 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10390 after ALTER, got {other:?}"
10391 )));
10392 }
10393 }
10394 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10395 // (mailrs migrate-042 ships these). The presence of an
10396 // IF EXISTS makes the subsequent name lookup tolerate
10397 // a missing index — engine returns CommandOk no-op.
10398 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10399 let next = self.tokens.get(self.pos + 1);
10400 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10401 self.advance();
10402 self.advance();
10403 true
10404 } else {
10405 false
10406 }
10407 } else {
10408 false
10409 };
10410 let name = self.expect_ident_like()?;
10411 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10412 // Detect BEFORE the REBUILD path so the existing REBUILD
10413 // arm stays untouched.
10414 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10415 self.advance();
10416 if matches!(self.peek(), Token::To) {
10417 self.advance();
10418 } else {
10419 self.expect_keyword_ident("to")?;
10420 }
10421 let new = self.expect_ident_like()?;
10422 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10423 name,
10424 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10425 }));
10426 }
10427 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10428 // A syntax error before; the index is validated, the params no-op.
10429 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10430 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10431 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10432 {
10433 self.consume_until_statement_boundary();
10434 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10435 name,
10436 target: crate::ast::AlterIndexTarget::StorageParams,
10437 }));
10438 }
10439 // REBUILD
10440 self.expect_keyword_ident("rebuild")?;
10441 // Optional: WITH (encoding = <enc>)
10442 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10443 self.advance();
10444 if !matches!(self.peek(), Token::LParen) {
10445 return Err(self.err(format!(
10446 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10447 self.peek()
10448 )));
10449 }
10450 self.advance();
10451 self.expect_keyword_ident("encoding")?;
10452 if !matches!(self.peek(), Token::Eq) {
10453 return Err(self.err(format!(
10454 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10455 self.peek()
10456 )));
10457 }
10458 self.advance();
10459 let enc_ident = match self.advance() {
10460 Token::Ident(s) | Token::QuotedIdent(s) => s,
10461 other => {
10462 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10463 }
10464 };
10465 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10466 "f32" => VecEncoding::F32,
10467 "sq8" => VecEncoding::Sq8,
10468 "half" => VecEncoding::F16,
10469 other => {
10470 return Err(self.err(format!(
10471 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10472 )));
10473 }
10474 };
10475 if !matches!(self.peek(), Token::RParen) {
10476 return Err(self.err(format!(
10477 "expected ')' after encoding value, got {:?}",
10478 self.peek()
10479 )));
10480 }
10481 self.advance();
10482 Some(enc)
10483 } else {
10484 None
10485 };
10486 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10487 name,
10488 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10489 }))
10490 }
10491
10492 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10493 /// only `SET` form currently supported; future v6.7.x can add
10494 /// more SET subjects without changing the dispatch shape.
10495 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10496 /// subactions. Single-subaction shape stays a 1-element vec.
10497 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10498 let table_name = self.expect_ident_like()?;
10499 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10500 loop {
10501 let subaction = self.parse_alter_table_subaction()?;
10502 // ADD COLUMN with inline REFERENCES emits both an
10503 // AddColumn and an AddForeignKey subaction; the
10504 // helper returns 1 or 2 items.
10505 targets.extend(subaction);
10506 if matches!(self.peek(), Token::Comma) {
10507 self.advance();
10508 continue;
10509 }
10510 break;
10511 }
10512 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10513 name: table_name,
10514 targets,
10515 }))
10516 }
10517
10518 /// Parse one ALTER TABLE subaction. Returns a Vec because
10519 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10520 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10521 fn parse_alter_table_subaction(
10522 &mut self,
10523 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10524 match self.peek() {
10525 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10526 self.advance();
10527 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10528 // storage parameters: paren-prefixed; consume.
10529 if matches!(self.peek(), Token::LParen) {
10530 self.consume_until_statement_boundary();
10531 return Ok(Vec::new());
10532 }
10533 let setting = self.expect_ident_like()?;
10534 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10535 if !matches!(self.peek(), Token::Eq) {
10536 return Err(self.err(alloc::format!(
10537 "expected '=' after hot_tier_bytes, got {:?}",
10538 self.peek()
10539 )));
10540 }
10541 self.advance();
10542 let n = self.expect_u64_literal()?;
10543 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10544 }
10545 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10546 // accept-and-no-op for ALTER TABLE SET <subject>
10547 // forms that pg_dump emits but SPG either treats
10548 // as N/A (single-tenant, single-owner, no shared
10549 // tablespaces) or accepts the dump-side declaration
10550 // without runtime effect:
10551 // SET SCHEMA <name> (18.11)
10552 // SET TABLESPACE <name> (18.8)
10553 // SET LOGGED / UNLOGGED (18.7 alt-form)
10554 // SET WITHOUT CLUSTER (18.13)
10555 // SET WITHOUT OIDS (PG legacy)
10556 // SET (option = value, …) (storage parameters)
10557 // SET REPLICA IDENTITY {…} (18.14)
10558 if setting.eq_ignore_ascii_case("schema")
10559 || setting.eq_ignore_ascii_case("tablespace")
10560 || setting.eq_ignore_ascii_case("logged")
10561 || setting.eq_ignore_ascii_case("unlogged")
10562 || setting.eq_ignore_ascii_case("without")
10563 {
10564 self.consume_until_statement_boundary();
10565 return Ok(Vec::new());
10566 }
10567 if setting.eq_ignore_ascii_case("replica") {
10568 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10569 self.consume_until_statement_boundary();
10570 return Ok(Vec::new());
10571 }
10572 // SET (option=value, …) — storage parameters.
10573 if matches!(self.peek(), Token::LParen) {
10574 self.consume_until_statement_boundary();
10575 return Ok(Vec::new());
10576 }
10577 Err(self.err(alloc::format!(
10578 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10579 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10580 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10581 )))
10582 }
10583 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10584 // not ignored: round 645 gave SPG the inheritance the
10585 // v7.37.18 no-op said it did not have.
10586 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10587 self.advance();
10588 let parent = self.expect_ident_like()?;
10589 self.consume_until_statement_boundary();
10590 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10591 parent,
10592 detach: false
10593 }])
10594 }
10595 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10596 // LEVEL SECURITY`, which has its own RLS arm below — without
10597 // the guard this swallowed NO FORCE as a no-op.
10598 Token::Ident(s)
10599 if s.eq_ignore_ascii_case("no")
10600 && !matches!(
10601 self.tokens.get(self.pos + 1),
10602 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10603 ) =>
10604 {
10605 self.advance();
10606 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10607 if k.eq_ignore_ascii_case("inherit"))
10608 {
10609 self.advance();
10610 let parent = self.expect_ident_like()?;
10611 self.consume_until_statement_boundary();
10612 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10613 parent,
10614 detach: true
10615 }]);
10616 }
10617 self.consume_until_statement_boundary();
10618 Ok(Vec::new())
10619 }
10620 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10621 // single-owner, so there is still nothing to record.
10622 //
10623 // v7.39 (round 652) — but the name now reaches the engine,
10624 // which refuses a role that does not exist as PG does. The
10625 // no-op was swallowing the whole statement, so a dump naming
10626 // a role this server never heard of restored clean and left
10627 // the table owned by whoever ran the restore.
10628 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10629 self.advance();
10630 if matches!(self.peek(), Token::To) {
10631 self.advance();
10632 }
10633 let role = self.expect_ident_like()?;
10634 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10635 role
10636 }])
10637 }
10638 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10639 // PG sets a hint; SPG doesn't have clustered storage, so the
10640 // hint itself stays a no-op.
10641 //
10642 // v7.39 (round 652) — the index name is checked now. PG
10643 // errors on one that does not exist, and swallowing that let
10644 // a typo'd CLUSTER ON pass silently.
10645 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10646 self.advance();
10647 // `ON` is a reserved token, not an ident.
10648 if !matches!(self.peek(), Token::On) {
10649 return Err(self.err(alloc::format!(
10650 "expected ON after CLUSTER, got {:?}",
10651 self.peek()
10652 )));
10653 }
10654 self.advance();
10655 let index = self.expect_ident_like()?;
10656 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10657 index: Some(index)
10658 }])
10659 }
10660 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10661 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10662 // what a logical decoder puts in the old-tuple image; SPG's
10663 // replication is SQL-text, so there is nothing to record.
10664 // Accept-and-no-op (it used to be a parse error).
10665 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10666 self.advance();
10667 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10668 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10669 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10670 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10671 {
10672 self.advance(); // IDENTITY
10673 self.advance(); // USING
10674 if matches!(self.peek(), Token::Index)
10675 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10676 {
10677 self.advance();
10678 }
10679 let index = self.expect_ident_like()?;
10680 self.consume_until_statement_boundary();
10681 return Ok(alloc::vec![
10682 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10683 ]);
10684 }
10685 self.consume_until_statement_boundary();
10686 Ok(Vec::new())
10687 }
10688 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10689 //
10690 // v7.39 (round 652) — it used to consume the statement and
10691 // return nothing, on the stated theory that SPG validated at
10692 // ADD CONSTRAINT time so there was never anything left to
10693 // validate. Measured against PG18, ADD CONSTRAINT did not
10694 // scan the existing rows at all — the comment described a
10695 // property SPG did not have, which is why nobody looked. Both
10696 // halves are real now: ADD scans unless told NOT VALID, and
10697 // this scans what NOT VALID skipped.
10698 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10699 self.advance();
10700 self.expect_keyword_ident("constraint")?;
10701 let name = self.expect_ident_like()?;
10702 Ok(alloc::vec![
10703 crate::ast::AlterTableTarget::ValidateConstraint { name }
10704 ])
10705 }
10706 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10707 // SET (option = value, …). PG uses it to clear per-table
10708 // storage params like fillfactor or autovacuum_*. SPG
10709 // engine-manages those parameters; accept-and-no-op.
10710 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10711 self.advance();
10712 self.consume_until_statement_boundary();
10713 Ok(Vec::new())
10714 }
10715 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10716 // type-of binding (PG 9.0+). SPG composite types
10717 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10718 // TABLE OF is rare and inverse of CREATE TABLE OF.
10719 // Accept-and-no-op until a customer dump round-trips it.
10720 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10721 self.advance();
10722 // v7.39 (round 710) — the type name is validated now.
10723 let type_name = self.expect_ident_like()?;
10724 self.consume_until_statement_boundary();
10725 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10726 type_name
10727 }])
10728 }
10729 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10730 // (reserved keyword) rather than Token::Ident("not"),
10731 // so it needs its own arm. Accept-and-no-op same as OF.
10732 Token::Not => {
10733 self.advance();
10734 self.consume_until_statement_boundary();
10735 Ok(Vec::new())
10736 }
10737 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10738 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10739 self.advance();
10740 self.expect_row_level_security()?;
10741 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10742 enabled: None,
10743 force: Some(true),
10744 }])
10745 }
10746 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10747 Token::Ident(s)
10748 if s.eq_ignore_ascii_case("no")
10749 && matches!(
10750 self.tokens.get(self.pos + 1),
10751 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10752 ) =>
10753 {
10754 self.advance(); // NO
10755 self.advance(); // FORCE
10756 self.expect_row_level_security()?;
10757 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10758 enabled: None,
10759 force: Some(false),
10760 }])
10761 }
10762 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10763 // (sets relrowsecurity). The guard requires the next token to be
10764 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10765 Token::Ident(s)
10766 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10767 && matches!(
10768 self.tokens.get(self.pos + 1),
10769 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10770 ) =>
10771 {
10772 let enabled = s.eq_ignore_ascii_case("enable");
10773 self.advance(); // ENABLE/DISABLE
10774 self.expect_row_level_security()?;
10775 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10776 enabled: Some(enabled),
10777 force: None,
10778 }])
10779 }
10780 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10781 self.advance();
10782 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10783 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10784 // emits. The same grammar CREATE TABLE already accepts
10785 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10786 // through the SAME parser — an ALTER-only copy would be a
10787 // second place for the two to drift.
10788 if self.peek_mysql_inline_key_start() {
10789 return Ok(match self.parse_mysql_inline_key()? {
10790 Some(c) => {
10791 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10792 }
10793 // FULLTEXT / SPATIAL parse and are accepted as a
10794 // no-op here exactly as they are inline.
10795 None => Vec::new(),
10796 });
10797 }
10798 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10799 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10800 // PRIMARY KEY this way; mysqldump emits both.
10801 // Peek-only dispatch (no advance) — `advance()`
10802 // destructively replaces consumed tokens with Eof,
10803 // so saved-pos restore would land on Eofs.
10804 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10805 {
10806 // The next-but-one ident is the constraint
10807 // name; the one after THAT is the kind.
10808 let kind_pos = self.pos + 2;
10809 let kind = self.tokens.get(kind_pos).cloned();
10810 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10811 {
10812 let fk = self.parse_table_level_fk()?;
10813 return Ok(alloc::vec![
10814 crate::ast::AlterTableTarget::AddForeignKey(fk)
10815 ]);
10816 }
10817 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10818 {
10819 self.advance(); // CONSTRAINT
10820 // v7.39 (read01 round 48) — keep the name; the engine
10821 // stores it now instead of dropping it on the floor.
10822 let con_name = self.expect_ident_like()?;
10823 self.advance(); // PRIMARY
10824 self.expect_keyword_ident("key")?;
10825 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10826 // v7.39 (round 711) — the ALTER form carries the
10827 // timing too (pg_dump writes it here).
10828 let (deferrable, initially_deferred) =
10829 self.consume_deferrable_clauses_timed()?;
10830 return Ok(alloc::vec![
10831 crate::ast::AlterTableTarget::AddTableConstraint(
10832 crate::ast::TableConstraint::PrimaryKey {
10833 name: Some(con_name),
10834 columns: cols,
10835 deferrable,
10836 initially_deferred,
10837 }
10838 )
10839 ]);
10840 }
10841 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10842 {
10843 self.advance(); // CONSTRAINT
10844 // v7.39 (read01 round 48) — keep the name.
10845 let con_name = self.expect_ident_like()?;
10846 // v7.22 (mailrs round-13 gap 6) — delegate so
10847 // the optional `NULLS [NOT] DISTINCT` modifier
10848 // parses here too (pg_dump emits the ALTER
10849 // form; semantics enforced by the engine
10850 // since v7.13).
10851 let mut uc = self.parse_table_level_unique()?;
10852 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10853 *name = Some(con_name);
10854 }
10855 return Ok(alloc::vec![
10856 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10857 ]);
10858 }
10859 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10860 {
10861 self.advance(); // CONSTRAINT
10862 // v7.39 (read01 round 48) — keep the name.
10863 let con_name = self.expect_ident_like()?;
10864 self.advance(); // CHECK
10865 if !matches!(self.peek(), Token::LParen) {
10866 return Err(self.err(alloc::format!(
10867 "expected '(' after CHECK, got {:?}", self.peek()
10868 )));
10869 }
10870 self.advance();
10871 let expr = self.parse_expr(0)?;
10872 if matches!(self.peek(), Token::RParen) {
10873 self.advance();
10874 }
10875 let not_valid = self.parse_not_valid_suffix();
10876 return Ok(alloc::vec![
10877 crate::ast::AlterTableTarget::AddTableConstraint(
10878 crate::ast::TableConstraint::Check {
10879 name: Some(con_name),
10880 expr,
10881 not_valid,
10882 }
10883 )
10884 ]);
10885 }
10886 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10887 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10888 // exclusion constraints via this ALTER form.
10889 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10890 {
10891 self.advance(); // CONSTRAINT
10892 let con_name = self.expect_ident_like()?;
10893 let mut ex = self.parse_table_level_exclude()?;
10894 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10895 *name = Some(con_name);
10896 }
10897 return Ok(alloc::vec![
10898 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10899 ]);
10900 }
10901 // Unknown kind — fall through to FK path which
10902 // produces a descriptive parse error.
10903 }
10904 let is_fk = matches!(
10905 self.peek(),
10906 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10907 || s.eq_ignore_ascii_case("foreign")
10908 );
10909 if is_fk {
10910 let fk = self.parse_table_level_fk()?;
10911 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10912 }
10913 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10914 // (no CONSTRAINT prefix) — same dispatch.
10915 match self.peek().clone() {
10916 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10917 self.advance();
10918 self.expect_keyword_ident("key")?;
10919 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10920 let (deferrable, initially_deferred) =
10921 self.consume_deferrable_clauses_timed()?;
10922 return Ok(alloc::vec![
10923 crate::ast::AlterTableTarget::AddTableConstraint(
10924 crate::ast::TableConstraint::PrimaryKey {
10925 name: None,
10926 columns: cols,
10927 deferrable,
10928 initially_deferred,
10929 }
10930 )
10931 ]);
10932 }
10933 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10934 // v7.22 — delegate (NULLS [NOT] DISTINCT).
10935 let uc = self.parse_table_level_unique()?;
10936 return Ok(alloc::vec![
10937 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10938 ]);
10939 }
10940 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10941 // prefix). The other three bare forms were here and
10942 // this one was not, so it fell through to the column
10943 // path and came back as "unexpected reserved keyword
10944 // 'check' at start of column definition".
10945 _ if self.peek_table_level_check_start() => {
10946 let chk = self.parse_table_level_check()?;
10947 let not_valid = self.parse_not_valid_suffix();
10948 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10949 unreachable!("parse_table_level_check returns Check")
10950 };
10951 return Ok(alloc::vec![
10952 crate::ast::AlterTableTarget::AddTableConstraint(
10953 crate::ast::TableConstraint::Check {
10954 name: None,
10955 expr,
10956 not_valid,
10957 }
10958 )
10959 ]);
10960 }
10961 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10962 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10963 let ex = self.parse_table_level_exclude()?;
10964 return Ok(alloc::vec![
10965 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10966 ]);
10967 }
10968 _ => {}
10969 }
10970 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10971 self.advance();
10972 }
10973 let mut if_not_exists = false;
10974 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10975 self.advance();
10976 if !matches!(self.peek(), Token::Not) {
10977 return Err(self.err(alloc::format!(
10978 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10979 self.peek()
10980 )));
10981 }
10982 self.advance();
10983 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10984 return Err(self.err(alloc::format!(
10985 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10986 self.peek()
10987 )));
10988 }
10989 self.advance();
10990 if_not_exists = true;
10991 }
10992 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
10993 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
10994 // returns ColumnDef + an optional inline FK.
10995 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
10996 let col_name = column.name.clone();
10997 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
10998 column,
10999 if_not_exists,
11000 }];
11001 if let Some(mut fk) = col_level_fk {
11002 if fk.columns.is_empty() {
11003 fk.columns.push(col_name);
11004 }
11005 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11006 }
11007 Ok(out)
11008 }
11009 Token::Drop => {
11010 self.advance();
11011 // v7.13.3 — dispatch on the next token. mailrs round-7
11012 // S8 closed DROP COLUMN; round-6 S7 closed
11013 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11014 // RESTRICT modifiers.
11015 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11016 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11017 let subject = match self.peek() {
11018 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11019 self.advance();
11020 "constraint"
11021 }
11022 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11023 self.advance();
11024 "column"
11025 }
11026 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11027 // `INDEX` lexes as the reserved Token::Index, so it is
11028 // unambiguous. `KEY` is a plain ident, and PG allows a
11029 // column literally named "key", so only read it as the
11030 // keyword when a name follows it.
11031 Token::Index => {
11032 self.advance();
11033 "index"
11034 }
11035 Token::Ident(s)
11036 if s.eq_ignore_ascii_case("key")
11037 && matches!(
11038 self.tokens.get(self.pos + 1),
11039 Some(Token::Ident(_) | Token::QuotedIdent(_))
11040 ) =>
11041 {
11042 self.advance();
11043 "index"
11044 }
11045 // PG-canonical bare `DROP <col>` without COLUMN
11046 // keyword is also valid; treat any other ident
11047 // as the column name.
11048 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11049 other => {
11050 return Err(self.err(alloc::format!(
11051 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11052 )));
11053 }
11054 };
11055 let mut if_exists = false;
11056 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11057 let n1 = self.tokens.get(self.pos + 1);
11058 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11059 self.advance();
11060 self.advance();
11061 if_exists = true;
11062 }
11063 }
11064 let name = self.expect_ident_like()?;
11065 let mut cascade = false;
11066 if matches!(
11067 self.peek(),
11068 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11069 || s.eq_ignore_ascii_case("restrict")
11070 ) {
11071 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11072 {
11073 cascade = true;
11074 }
11075 self.advance();
11076 }
11077 if subject == "index" {
11078 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11079 name,
11080 if_exists,
11081 }])
11082 } else if subject == "constraint" {
11083 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11084 name,
11085 if_exists,
11086 }])
11087 } else {
11088 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11089 column: name,
11090 if_exists,
11091 cascade,
11092 }])
11093 }
11094 }
11095 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11096 self.advance();
11097 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11098 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11099 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11100 // immediately; accept-and-no-op.
11101 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11102 self.advance();
11103 self.consume_until_statement_boundary();
11104 return Ok(Vec::new());
11105 }
11106 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11107 self.advance();
11108 }
11109 let col_name = self.expect_ident_like()?;
11110 match self.peek() {
11111 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11112 self.advance();
11113 }
11114 // v7.14.0 — pg_dump emits BIGSERIAL via
11115 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11116 // nextval('seq')` (the sequence is created
11117 // separately). SPG's BIGSERIAL already uses
11118 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11119 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11120 // engine no-ops by consuming the tail.
11121 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11122 // v7.22 (round-13 T2) — `SET DEFAULT
11123 // nextval('…')` is how pg_dump spells a
11124 // SERIAL column (plain integer in CREATE
11125 // TABLE + this ALTER). It used to be
11126 // swallowed as a no-op, which silently
11127 // STRIPPED auto-increment from imported
11128 // schemas — the first post-import INSERT
11129 // without an explicit id then violated NOT
11130 // NULL. Lower it to the auto-increment
11131 // marker instead.
11132 let is_default_nextval =
11133 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11134 && matches!(
11135 self.tokens.get(self.pos + 2),
11136 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11137 );
11138 if is_default_nextval {
11139 let seq_name = self.scan_sequence_name_until_boundary();
11140 return Ok(alloc::vec![
11141 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11142 column: col_name,
11143 seq_name,
11144 }
11145 ]);
11146 }
11147 // v7.37.18 (18.1 + 18.2) — proper lowering.
11148 self.advance(); // consume "set"
11149 match self.peek().clone() {
11150 Token::Default => {
11151 self.advance();
11152 let default_expr = self.parse_expr(0)?;
11153 return Ok(alloc::vec![
11154 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11155 column: col_name,
11156 default_expr,
11157 }
11158 ]);
11159 }
11160 Token::Not => {
11161 self.advance();
11162 if !matches!(self.peek(), Token::Null) {
11163 return Err(self.err(alloc::format!(
11164 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11165 self.peek()
11166 )));
11167 }
11168 self.advance();
11169 return Ok(alloc::vec![
11170 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11171 column: col_name,
11172 }
11173 ]);
11174 }
11175 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11176 // stored generated column's expression and
11177 // recompute existing rows.
11178 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11179 self.advance(); // EXPRESSION
11180 if matches!(self.peek(), Token::As) {
11181 self.advance();
11182 }
11183 let expr = self.parse_expr(0)?;
11184 return Ok(alloc::vec![
11185 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11186 column: col_name,
11187 expr,
11188 }
11189 ]);
11190 }
11191 other => {
11192 // Other SET subjects (STATISTICS,
11193 // STORAGE, COMPRESSION, …) stay no-ops —
11194 // storage hints with no SPG semantics.
11195 let _ = other;
11196 self.consume_until_statement_boundary();
11197 return Ok(Vec::new());
11198 }
11199 }
11200 }
11201 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11202 self.advance(); // consume "drop"
11203 return self.parse_alter_column_drop_tail(col_name);
11204 }
11205 Token::Drop => {
11206 self.advance(); // consume Drop token
11207 return self.parse_alter_column_drop_tail(col_name);
11208 }
11209 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11210 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11211 // GENERATED { ALWAYS | BY DEFAULT } AS
11212 // IDENTITY ( … )`: pg_dump's spelling for
11213 // identity columns. Same auto-increment
11214 // lowering as the nextval default; the
11215 // sequence options inside the parens are
11216 // no-ops under SPG's max+1 semantics.
11217 let is_generated = matches!(
11218 self.tokens.get(self.pos + 1),
11219 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11220 );
11221 if !is_generated {
11222 return Err(self.err(alloc::format!(
11223 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11224 self.tokens.get(self.pos + 1)
11225 )));
11226 }
11227 let seq_name = self.scan_sequence_name_until_boundary();
11228 return Ok(alloc::vec![
11229 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11230 column: col_name,
11231 seq_name,
11232 }
11233 ]);
11234 }
11235 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11236 // column: floor the next allocated value at n (bare
11237 // RESTART = restart from the start value, 1).
11238 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11239 self.advance();
11240 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11241 {
11242 self.advance();
11243 let neg = if matches!(self.peek(), Token::Minus) {
11244 self.advance();
11245 true
11246 } else {
11247 false
11248 };
11249 match self.advance() {
11250 Token::Integer(v) => Some(if neg { -v } else { v }),
11251 other => {
11252 return Err(self.err(alloc::format!(
11253 "expected integer after RESTART WITH, got {other:?}"
11254 )));
11255 }
11256 }
11257 } else {
11258 None
11259 };
11260 return Ok(alloc::vec![
11261 crate::ast::AlterTableTarget::AlterColumnRestart {
11262 column: col_name,
11263 with,
11264 }
11265 ]);
11266 }
11267 other => {
11268 return Err(self.err(alloc::format!(
11269 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11270 )));
11271 }
11272 }
11273 // v7.39 (round 713) — the type parser has consumed a
11274 // trailing `COLLATE <name>` since Phase 2.5, and
11275 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11276 // TYPE text COLLATE "C"` parsed clean and changed
11277 // nothing. Keep the clause; the engine re-collates.
11278 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11279 self.parse_type_with_implied_flags()?;
11280 let collation = if coll_explicit {
11281 coll_name.map(|n| (coll, n))
11282 } else {
11283 None
11284 };
11285 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11286 {
11287 self.advance();
11288 Some(self.parse_expr(0)?)
11289 } else {
11290 None
11291 };
11292 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11293 column: col_name,
11294 new_type,
11295 using,
11296 collation,
11297 }])
11298 }
11299 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11300 // PG also supports `RENAME TO new_table` for table-name
11301 // rename; that surface is deferred (pg_dump never emits
11302 // it). If the first post-RENAME ident is `TO`, the user
11303 // is asking for table rename — error with a clear
11304 // message rather than misparsing `TO` as a column name.
11305 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11306 self.advance();
11307 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11308 // table-name rename (mailrs round-10 A.5 — used
11309 // by migrate-042's `RENAME TO email_contacts`).
11310 // `TO` lexes as Token::To.
11311 if matches!(self.peek(), Token::To)
11312 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11313 {
11314 self.advance();
11315 let new = self.expect_ident_like()?;
11316 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11317 new,
11318 }]);
11319 }
11320 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11321 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11322 self.advance();
11323 let old = self.expect_ident_like()?;
11324 if matches!(self.peek(), Token::To) {
11325 self.advance();
11326 } else {
11327 self.expect_keyword_ident("to")?;
11328 }
11329 let new = self.expect_ident_like()?;
11330 return Ok(alloc::vec![
11331 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11332 ]);
11333 }
11334 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11335 self.advance();
11336 }
11337 let old = self.expect_ident_like()?;
11338 // `TO` is a reserved keyword token; accept both
11339 // Token::To and Token::Ident("to") for consistency.
11340 if matches!(self.peek(), Token::To) {
11341 self.advance();
11342 } else {
11343 self.expect_keyword_ident("to")?;
11344 }
11345 let new = self.expect_ident_like()?;
11346 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11347 old,
11348 new,
11349 }])
11350 }
11351 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11352 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11353 // every data block with these. Real disable semantics —
11354 // not no-op — because reload correctness assumes the
11355 // triggers don't fire (rows already carry their
11356 // computed values from prod).
11357 Token::Ident(s)
11358 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11359 {
11360 let enabled = s.eq_ignore_ascii_case("enable");
11361 self.advance();
11362 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11363 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11364 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11365 // pg_dump output) — anything else falls through to
11366 // the catch-all error below.
11367 // v7.22 (round-13 T3) — mysqldump wraps every data
11368 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11369 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11370 // maintains indexes incrementally — engine no-op.
11371 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11372 self.advance();
11373 return Ok(Vec::new());
11374 }
11375 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11376 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11377 // to gate triggers on session_replication_role; SPG
11378 // has no replica role, so the prefix is consumed and
11379 // treated identically to the plain ENABLE/DISABLE
11380 // TRIGGER form.
11381 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11382 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11383 {
11384 self.advance();
11385 }
11386 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11387 return Err(self.err(alloc::format!(
11388 "expected TRIGGER after {}, got {:?}",
11389 if enabled { "ENABLE" } else { "DISABLE" },
11390 self.peek()
11391 )));
11392 }
11393 self.advance();
11394 // `ALL` lexes as Token::All (reserved); also
11395 // accept Token::Ident("all") for symmetry.
11396 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11397 // TRIGGER selectors. USER (= all user triggers) is
11398 // semantically ALL here; REPLICA / ALWAYS gate on
11399 // session_replication_role which SPG doesn't track.
11400 // All map to TriggerSelector::All.
11401 let which = if matches!(self.peek(), Token::All)
11402 || matches!(self.peek(), Token::Ident(s)
11403 if s.eq_ignore_ascii_case("all")
11404 || s.eq_ignore_ascii_case("user")
11405 || s.eq_ignore_ascii_case("replica")
11406 || s.eq_ignore_ascii_case("always"))
11407 {
11408 self.advance();
11409 crate::ast::TriggerSelector::All
11410 } else {
11411 let name = self.expect_ident_like()?;
11412 crate::ast::TriggerSelector::Named(name)
11413 };
11414 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11415 which,
11416 enabled,
11417 }])
11418 }
11419 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11420 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11421 self.advance();
11422 if !matches!(self.peek(), Token::Partition)
11423 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11424 if s.eq_ignore_ascii_case("partition"))
11425 {
11426 return Err(self.err(alloc::format!(
11427 "expected PARTITION after ATTACH, got {:?}",
11428 self.peek()
11429 )));
11430 }
11431 self.advance();
11432 let child = self.expect_ident_like()?;
11433 let bounds = self.parse_partition_bounds_tail()?;
11434 Ok(alloc::vec![
11435 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11436 ])
11437 }
11438 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11439 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11440 self.advance();
11441 if !matches!(self.peek(), Token::Partition)
11442 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11443 if s.eq_ignore_ascii_case("partition"))
11444 {
11445 return Err(self.err(alloc::format!(
11446 "expected PARTITION after DETACH, got {:?}",
11447 self.peek()
11448 )));
11449 }
11450 self.advance();
11451 let child = self.expect_ident_like()?;
11452 let mut concurrently = false;
11453 let mut finalize = false;
11454 loop {
11455 match self.peek().clone() {
11456 Token::Ident(s) | Token::QuotedIdent(s)
11457 if s.eq_ignore_ascii_case("concurrently") =>
11458 {
11459 self.advance();
11460 concurrently = true;
11461 }
11462 Token::Ident(s) | Token::QuotedIdent(s)
11463 if s.eq_ignore_ascii_case("finalize") =>
11464 {
11465 self.advance();
11466 finalize = true;
11467 }
11468 _ => break,
11469 }
11470 }
11471 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11472 child,
11473 concurrently,
11474 finalize,
11475 }])
11476 }
11477 other => Err(self.err(alloc::format!(
11478 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11479 ))),
11480 }
11481 }
11482
11483 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11484 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11485 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11486 /// `parse_partition_of_tail`'s bounds branch.
11487 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11488 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11489 /// lowering each to the respective AlterTableTarget. Any
11490 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11491 /// no-op via consume_until_statement_boundary.
11492 fn parse_alter_column_drop_tail(
11493 &mut self,
11494 col_name: String,
11495 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11496 match self.peek().clone() {
11497 Token::Default => {
11498 self.advance();
11499 Ok(alloc::vec![
11500 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11501 ])
11502 }
11503 Token::Not => {
11504 self.advance();
11505 if !matches!(self.peek(), Token::Null) {
11506 return Err(self.err(alloc::format!(
11507 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11508 self.peek()
11509 )));
11510 }
11511 self.advance();
11512 Ok(alloc::vec![
11513 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11514 ])
11515 }
11516 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11517 // generated column into a plain column.
11518 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11519 self.advance();
11520 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11521 // dropped, so the engine still errored on a plain
11522 // column; PG's semantics are NOTICE + skip.
11523 let mut if_exists = false;
11524 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11525 self.advance();
11526 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11527 self.advance();
11528 if_exists = true;
11529 }
11530 }
11531 Ok(alloc::vec![
11532 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11533 column: col_name,
11534 if_exists,
11535 }
11536 ])
11537 }
11538 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11539 // identity column into a plain column.
11540 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11541 self.advance();
11542 let mut if_exists = false;
11543 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11544 self.advance();
11545 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11546 self.advance();
11547 if_exists = true;
11548 }
11549 }
11550 Ok(alloc::vec![
11551 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11552 column: col_name,
11553 if_exists,
11554 }
11555 ])
11556 }
11557 _ => {
11558 self.consume_until_statement_boundary();
11559 Ok(Vec::new())
11560 }
11561 }
11562 }
11563
11564 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11565 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11566 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11567 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11568 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11569 let mut opts = crate::ast::CopyOptions::default();
11570 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11571 return Ok(opts);
11572 }
11573 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11574 self.advance();
11575 }
11576 if matches!(self.peek(), Token::LParen) {
11577 self.advance();
11578 loop {
11579 self.parse_one_copy_option(&mut opts)?;
11580 match self.peek() {
11581 Token::Comma => {
11582 self.advance();
11583 }
11584 Token::RParen => {
11585 self.advance();
11586 break;
11587 }
11588 other => {
11589 return Err(self.err(alloc::format!(
11590 "expected ',' or ')' in COPY options, got {other:?}"
11591 )));
11592 }
11593 }
11594 }
11595 } else {
11596 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11597 self.parse_one_copy_option(&mut opts)?;
11598 }
11599 }
11600 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11601 return Err(self.err(alloc::format!(
11602 "unexpected token after COPY options: {:?}",
11603 self.peek()
11604 )));
11605 }
11606 Ok(opts)
11607 }
11608
11609 fn parse_one_copy_option(
11610 &mut self,
11611 opts: &mut crate::ast::CopyOptions,
11612 ) -> Result<(), ParseError> {
11613 use crate::ast::CopyFormat;
11614 // The option keyword. NULL lexes as its own token; the rest are
11615 // bare identifiers.
11616 let kw = match self.advance() {
11617 Token::Null => alloc::string::String::from("NULL"),
11618 Token::Ident(s) => s.to_uppercase(),
11619 other => {
11620 return Err(self.err(alloc::format!(
11621 "expected a COPY option keyword, got {other:?}"
11622 )));
11623 }
11624 };
11625 match kw.as_str() {
11626 "FORMAT" => {
11627 let fmt = self.expect_ident_like()?;
11628 match fmt.to_ascii_uppercase().as_str() {
11629 "CSV" => opts.format = CopyFormat::Csv,
11630 "TEXT" => opts.format = CopyFormat::Text,
11631 other => {
11632 return Err(self.err(alloc::format!(
11633 "COPY format \"{}\" not recognized",
11634 other.to_ascii_lowercase()
11635 )));
11636 }
11637 }
11638 }
11639 // Legacy bare format keywords.
11640 "CSV" => opts.format = CopyFormat::Csv,
11641 "TEXT" => opts.format = CopyFormat::Text,
11642 "HEADER" => {
11643 opts.header = match self.peek() {
11644 Token::True => {
11645 self.advance();
11646 true
11647 }
11648 Token::False => {
11649 self.advance();
11650 false
11651 }
11652 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11653 self.advance();
11654 true
11655 }
11656 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11657 self.advance();
11658 false
11659 }
11660 // Bare HEADER (no boolean) means HEADER true.
11661 _ => true,
11662 };
11663 }
11664 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11665 let s = match self.advance() {
11666 Token::String(s) => s,
11667 other => {
11668 return Err(self.err(alloc::format!(
11669 "COPY {kw} expects a single-character string, got {other:?}"
11670 )));
11671 }
11672 };
11673 // v7.39 (round 247) — PG's wording (0A000), keyword in
11674 // lowercase: "COPY delimiter must be a single one-byte
11675 // character".
11676 let one_byte_err = || {
11677 self.err(alloc::format!(
11678 "COPY {} must be a single one-byte character",
11679 kw.to_ascii_lowercase()
11680 ))
11681 };
11682 let mut chars = s.chars();
11683 let c = chars.next().ok_or_else(one_byte_err)?;
11684 if chars.next().is_some() || c.len_utf8() != 1 {
11685 return Err(one_byte_err());
11686 }
11687 match kw.as_str() {
11688 "DELIMITER" => opts.delimiter = Some(c),
11689 "QUOTE" => opts.quote = Some(c),
11690 _ => opts.escape = Some(c),
11691 }
11692 }
11693 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11694 "FORCE_QUOTE" => {
11695 if matches!(self.peek(), Token::Star) {
11696 self.advance();
11697 opts.force_quote = Some(Vec::new());
11698 } else {
11699 if !matches!(self.peek(), Token::LParen) {
11700 return Err(self.err(alloc::format!(
11701 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11702 self.peek()
11703 )));
11704 }
11705 self.advance();
11706 let mut cols = Vec::new();
11707 loop {
11708 cols.push(self.expect_ident_like()?);
11709 match self.peek() {
11710 Token::Comma => {
11711 self.advance();
11712 }
11713 Token::RParen => {
11714 self.advance();
11715 break;
11716 }
11717 other => {
11718 return Err(self.err(alloc::format!(
11719 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11720 )));
11721 }
11722 }
11723 }
11724 opts.force_quote = Some(cols);
11725 }
11726 }
11727 "NULL" => {
11728 opts.null_str = Some(match self.advance() {
11729 Token::String(s) => s,
11730 other => {
11731 return Err(self.err(alloc::format!(
11732 "COPY NULL expects a quoted string, got {other:?}"
11733 )));
11734 }
11735 });
11736 }
11737 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11738 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11739 // FORCE_NULL too.
11740 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11741 let cols = self.parse_copy_column_list(&kw)?;
11742 if kw == "FORCE_NOT_NULL" {
11743 opts.force_not_null = Some(cols);
11744 } else {
11745 opts.force_null = Some(cols);
11746 }
11747 }
11748 other => {
11749 // PG's wording, lowercased option name.
11750 return Err(self.err(alloc::format!(
11751 "option \"{}\" not recognized",
11752 other.to_ascii_lowercase()
11753 )));
11754 }
11755 }
11756 Ok(())
11757 }
11758
11759 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11760 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11761 /// is the `*` spelling.
11762 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11763 if matches!(self.peek(), Token::Star) {
11764 self.advance();
11765 return Ok(Vec::new());
11766 }
11767 if !matches!(self.peek(), Token::LParen) {
11768 return Err(self.err(alloc::format!(
11769 "expected '(' or '*' after {kw}, got {:?}",
11770 self.peek()
11771 )));
11772 }
11773 self.advance();
11774 let mut cols = Vec::new();
11775 loop {
11776 cols.push(self.expect_ident_like()?);
11777 match self.peek() {
11778 Token::Comma => {
11779 self.advance();
11780 }
11781 Token::RParen => {
11782 self.advance();
11783 break;
11784 }
11785 other => {
11786 return Err(self.err(alloc::format!(
11787 "expected ',' or ')' in {kw} list, got {other:?}"
11788 )));
11789 }
11790 }
11791 }
11792 Ok(cols)
11793 }
11794
11795 fn parse_partition_bounds_tail(
11796 &mut self,
11797 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11798 use crate::ast::PartitionOfBoundsAst;
11799 match self.peek() {
11800 Token::Default => {
11801 self.advance();
11802 Ok(PartitionOfBoundsAst::Default)
11803 }
11804 Token::For => {
11805 self.advance();
11806 if !matches!(self.peek(), Token::Values) {
11807 return Err(
11808 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11809 );
11810 }
11811 self.advance();
11812 let want_with = matches!(
11813 self.peek(),
11814 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11815 );
11816 if want_with {
11817 self.advance();
11818 if !matches!(self.peek(), Token::LParen) {
11819 return Err(self.err(format!(
11820 "expected '(' after FOR VALUES WITH, got {:?}",
11821 self.peek()
11822 )));
11823 }
11824 self.advance();
11825 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11826 loop {
11827 let key = self.expect_ident_like()?;
11828 let n = match self.peek().clone() {
11829 Token::Integer(v) if u32::try_from(v).is_ok() => {
11830 self.advance();
11831 v as u32
11832 }
11833 other => {
11834 return Err(self.err(format!(
11835 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11836 )));
11837 }
11838 };
11839 match key.to_ascii_uppercase().as_str() {
11840 "MODULUS" => modulus = Some(n),
11841 "REMAINDER" => remainder = Some(n),
11842 other => {
11843 return Err(self.err(format!(
11844 "FOR VALUES WITH: unknown key {other:?}; \
11845 expected MODULUS or REMAINDER"
11846 )));
11847 }
11848 }
11849 match self.peek() {
11850 Token::Comma => {
11851 self.advance();
11852 }
11853 Token::RParen => {
11854 self.advance();
11855 break;
11856 }
11857 other => {
11858 return Err(self.err(format!(
11859 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11860 )));
11861 }
11862 }
11863 }
11864 let modulus = modulus
11865 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11866 let remainder = remainder.ok_or_else(|| {
11867 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11868 })?;
11869 if modulus == 0 {
11870 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11871 }
11872 if remainder >= modulus {
11873 return Err(self.err(format!(
11874 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11875 )));
11876 }
11877 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11878 }
11879 match self.peek() {
11880 Token::From => {
11881 self.advance();
11882 let lower = Box::new(self.parse_partition_bound_expr()?);
11883 if !matches!(self.peek(), Token::To) {
11884 return Err(self.err(format!(
11885 "expected TO after FROM (...), got {:?}",
11886 self.peek()
11887 )));
11888 }
11889 self.advance();
11890 let upper = Box::new(self.parse_partition_bound_expr()?);
11891 Ok(PartitionOfBoundsAst::Range { lower, upper })
11892 }
11893 Token::In => {
11894 self.advance();
11895 if !matches!(self.peek(), Token::LParen) {
11896 return Err(self.err(format!(
11897 "expected '(' after FOR VALUES IN, got {:?}",
11898 self.peek()
11899 )));
11900 }
11901 self.advance();
11902 let mut values = Vec::new();
11903 loop {
11904 values.push(self.parse_expr(0)?);
11905 match self.peek() {
11906 Token::Comma => {
11907 self.advance();
11908 }
11909 Token::RParen => {
11910 self.advance();
11911 break;
11912 }
11913 other => {
11914 return Err(self.err(format!(
11915 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11916 )));
11917 }
11918 }
11919 }
11920 if values.is_empty() {
11921 return Err(
11922 self.err("FOR VALUES IN requires at least one literal".to_string())
11923 );
11924 }
11925 Ok(PartitionOfBoundsAst::List { values })
11926 }
11927 other => Err(self.err(format!(
11928 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11929 ))),
11930 }
11931 }
11932 other => Err(self.err(format!(
11933 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11934 ))),
11935 }
11936 }
11937
11938 /// v7.16.2 — peek for `information_schema.<tbl>` /
11939 /// `pg_catalog.<tbl>` triples and, if matched, consume all
11940 /// three tokens + return a synthetic table name the engine's
11941 /// SELECT path recognises as a virtual view. Returns `None`
11942 /// when the head doesn't look like a meta-qualified name.
11943 /// Used by `parse_table_ref` to bypass the
11944 /// `expect_ident_like` schema-strip for these specific PG
11945 /// meta schemas (mailrs round-10 A.3).
11946 fn try_peek_meta_qualified(&mut self) -> Option<String> {
11947 // Extract the schema name. Must be a plain ident token.
11948 let schema = match self.tokens.get(self.pos) {
11949 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11950 _ => return None,
11951 };
11952 // Dot.
11953 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11954 return None;
11955 }
11956 // The table-side ident may lex as a reserved keyword
11957 // (e.g. `Token::Tables`). Tolerate the common ones via a
11958 // helper that reads the trailing token's underlying name.
11959 let tbl = match self.tokens.get(self.pos + 2)? {
11960 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11961 Token::Tables => "tables".to_string(),
11962 // Other PG meta table names that may collide with
11963 // reserved keywords land here as needed.
11964 _ => return None,
11965 };
11966 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11967 // names so the synthetic name doesn't double-prefix
11968 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11969 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11970 ("__spg_info_", tbl.to_ascii_lowercase())
11971 } else if schema.eq_ignore_ascii_case("pg_catalog") {
11972 // v7.39 (round 541) — only the catalogs SPG actually
11973 // synthesises are rewritten, which is what the BARE path
11974 // has always checked. Anything else keeps its own name and
11975 // takes the ordinary route: `pg_stat_activity` and friends
11976 // resolve through meta_view_result, and a name that is no
11977 // catalog at all gets PG's "relation does not exist"
11978 // instead of a message about a view SPG cannot materialise.
11979 let lowered = tbl.to_ascii_lowercase();
11980 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11981 self.advance(); // schema
11982 self.advance(); // dot
11983 self.advance(); // tbl
11984 return Some(lowered);
11985 }
11986 let bare = lowered
11987 .strip_prefix("pg_")
11988 .map(alloc::string::String::from)
11989 .unwrap_or(lowered);
11990 ("__spg_pg_", bare)
11991 } else if schema.eq_ignore_ascii_case("mysql") {
11992 // v7.17.0 Phase 3.P0-65 — MySQL system schema
11993 // (`mysql.user`, `mysql.db`). Same synthetic-name
11994 // shape as pg_catalog.
11995 ("__spg_mysql_", tbl.to_ascii_lowercase())
11996 } else {
11997 return None;
11998 };
11999 self.advance(); // schema
12000 self.advance(); // dot
12001 self.advance(); // tbl
12002 Some(alloc::format!("{prefix}{normalised}"))
12003 }
12004
12005 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12006 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12007 /// implicit front of every search_path, so a bare reference to a
12008 /// known catalog table always means the catalog table. Only the
12009 /// names the engine actually synthesises are recognised — any
12010 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12011 fn try_peek_meta_bare(&mut self) -> Option<String> {
12012 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12013 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12014 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12015 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12016 // through the meta_view_result path instead, and already resolve
12017 // bare — they must NOT be listed here or the __spg_ rewrite would
12018 // mis-target them.)
12019 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12020 let name = match self.tokens.get(self.pos) {
12021 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12022 _ => return None,
12023 };
12024 // A following dot means this ident is a schema qualifier,
12025 // not a table name — let the qualified path handle it.
12026 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12027 return None;
12028 }
12029 if !PG_META_TABLES.contains(&name.as_str()) {
12030 return None;
12031 }
12032 self.advance();
12033 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12034 Some(alloc::format!("__spg_pg_{bare}"))
12035 }
12036
12037 /// Consume a bare ident if its lowercase matches `kw`, else err.
12038 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12039 /// Peeks only; the caller advances.
12040 fn peek_keyword_ident(&self, kw: &str) -> bool {
12041 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12042 }
12043
12044 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12045 match self.advance() {
12046 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12047 other => Err(ParseError {
12048 message: format!("expected {kw:?}, got {other:?}"),
12049 token_pos: self.consumed_pos(),
12050 }),
12051 }
12052 }
12053
12054 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12055 /// literal (`'foo'`) — same shape used by CREATE USER for the
12056 /// username slot.
12057 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12058 match self.advance() {
12059 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12060 other => Err(ParseError {
12061 message: format!("expected identifier or string, got {other:?}"),
12062 token_pos: self.consumed_pos(),
12063 }),
12064 }
12065 }
12066
12067 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12068 match self.advance() {
12069 Token::String(s) => Ok(s),
12070 other => Err(ParseError {
12071 message: format!("expected quoted string, got {other:?}"),
12072 token_pos: self.consumed_pos(),
12073 }),
12074 }
12075 }
12076
12077 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12078 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12079 // subqueries recurse through here without passing
12080 // parse_expr; share the same nesting budget.
12081 self.enter_nested()?;
12082 let r = self.parse_select_stmt_inner();
12083 self.nest_depth -= 1;
12084 r
12085 }
12086
12087 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12088 // Caller dispatches on Token::Select; the inner helper handles
12089 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12090 // get a fresh bare-select parse and may not have their own ORDER
12091 // BY / LIMIT.
12092 let mut head = self.parse_bare_select()?;
12093 self.parse_setop_chain_into(&mut head)?;
12094 self.parse_select_tail_into(&mut head)?;
12095 Ok(Statement::Select(head))
12096 }
12097
12098 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12099 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12100 /// token), and INTERSECT [ALL] (a bare ident — it was never
12101 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12102 /// tighter than UNION / EXCEPT — the executor folds the chain
12103 /// left-to-right, which is already correct for LEADING
12104 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12105 /// pair nests into that previous peer, so A UNION B INTERSECT C
12106 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12107 /// groups.
12108 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12109 // A parenthesized group arrives with its own (already
12110 // regrouped) unions on `head`; only the pairs THIS chain
12111 // appends participate in the precedence regroup below —
12112 // nesting an outer INTERSECT into a group-internal peer
12113 // would dissolve the explicit grouping.
12114 let boundary = head.unions.len();
12115 loop {
12116 let base = match self.peek() {
12117 Token::Union => UnionKind::Distinct,
12118 Token::Except => UnionKind::Except,
12119 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12120 _ => break,
12121 };
12122 self.advance();
12123 let kind = if matches!(self.peek(), Token::All) {
12124 self.advance();
12125 match base {
12126 UnionKind::Distinct => UnionKind::All,
12127 UnionKind::Except => UnionKind::ExceptAll,
12128 _ => UnionKind::IntersectAll,
12129 }
12130 } else {
12131 base
12132 };
12133 let peer = self.parse_bare_select()?;
12134 head.unions.push((kind, peer));
12135 }
12136 let mut pairs = core::mem::take(&mut head.unions);
12137 let tail = pairs.split_off(boundary);
12138 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12139 for (kind, peer) in tail {
12140 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12141 // An intersect nests into the previous element of THIS
12142 // chain only; with no new previous element it stays at
12143 // the outer level (the left fold applies it to the
12144 // whole head, group included).
12145 match (
12146 is_intersect,
12147 regrouped.len() > boundary,
12148 regrouped.last_mut(),
12149 ) {
12150 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12151 _ => regrouped.push((kind, peer)),
12152 }
12153 }
12154 head.unions = regrouped;
12155 Ok(())
12156 }
12157
12158 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12159 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12160 /// the top-level bare VALUES statement reuses it verbatim.
12161 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12162 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12163 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12164 /// where the grouping-set universe is still in scope.
12165 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12166 if !matches!(self.peek(), Token::Order) {
12167 return Ok(Vec::new());
12168 }
12169 self.advance();
12170 if !self.peek_is_by() {
12171 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12172 }
12173 self.advance();
12174 let mut keys = Vec::new();
12175 loop {
12176 // v7.39 (round 691) — save/restore, the discipline this parser
12177 // already uses around `pending_sample_preds`, so a subquery inside
12178 // a key neither inherits nor leaks the channel.
12179 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12180 let saved_coll = self.order_key_collation.take();
12181 let parsed = self.parse_expr(0);
12182 self.in_order_by_key = saved_flag;
12183 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12184 let expr = parsed?;
12185 let desc = if matches!(self.peek(), Token::Desc) {
12186 self.advance();
12187 true
12188 } else if matches!(self.peek(), Token::Asc) {
12189 self.advance();
12190 false
12191 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12192 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12193 // one ordering per type, so the btree comparison operators map
12194 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12195 // would need a custom operator class — honest error.
12196 self.advance();
12197 match self.advance() {
12198 Token::Lt | Token::LtEq => false,
12199 Token::Gt | Token::GtEq => true,
12200 other => {
12201 return Err(self.err(alloc::format!(
12202 "ORDER BY USING supports the btree comparison \
12203 operators (< <= > >=); got {other:?}"
12204 )));
12205 }
12206 }
12207 } else {
12208 false
12209 };
12210 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12211 let nulls_first = self.parse_optional_nulls_placement()?;
12212 keys.push(OrderBy {
12213 expr,
12214 desc,
12215 nulls_first,
12216 collation,
12217 });
12218 if matches!(self.peek(), Token::Comma) {
12219 self.advance();
12220 } else {
12221 break;
12222 }
12223 }
12224 Ok(keys)
12225 }
12226
12227 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12228 // v7.39 (round 135) — a grouping-set query may have already parsed +
12229 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12230 // no ORDER BY token is present, keep that pre-set order_by rather than
12231 // clobbering it with an empty list.
12232 let parsed_keys = self.parse_order_by_keys()?;
12233 head.order_by = if parsed_keys.is_empty() {
12234 core::mem::take(&mut head.order_by)
12235 } else {
12236 parsed_keys
12237 };
12238 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12239 // order. PG's grammar takes a limit clause and an offset clause
12240 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12241 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12242 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12243 // spelling died on `expected end of input, got Limit`.
12244 //
12245 // Each may appear at most once, and LIMIT and FETCH FIRST are
12246 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12247 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12248 // A second one is left unconsumed here, which the caller reports
12249 // as trailing input rather than silently taking the last.
12250 let mut saw_limit = false;
12251 let mut saw_offset = false;
12252 loop {
12253 if !saw_limit && matches!(self.peek(), Token::Limit) {
12254 self.advance();
12255 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12256 // PG synonyms for "no limit". Treat both as None
12257 // (no head.limit set) so the engine's existing
12258 // unlimited-result path takes over. Reject was the
12259 // pre-5.1 behaviour and broke pg_dump-flavoured
12260 // tooling that occasionally emits LIMIT NULL.
12261 if self.consume_limit_unbounded_sentinel() {
12262 head.limit = None;
12263 } else {
12264 let first = self.parse_limit_expr("LIMIT")?;
12265 // MySQL `LIMIT offset, count` — the first number is
12266 // the offset when a comma follows.
12267 if matches!(self.peek(), Token::Comma) {
12268 self.advance();
12269 let count = self.parse_limit_expr("LIMIT")?;
12270 head.offset = Some(first);
12271 saw_offset = true;
12272 head.limit = Some(count);
12273 } else {
12274 head.limit = Some(first);
12275 }
12276 }
12277 saw_limit = true;
12278 continue;
12279 }
12280 if !saw_offset && matches!(self.peek(), Token::Offset) {
12281 self.advance();
12282 // PG also accepts an optional `ROW` / `ROWS` trailer
12283 // after the offset value (`OFFSET 10 ROWS`). The
12284 // FETCH-FIRST branch below relies on the same.
12285 let off = self.parse_limit_expr("OFFSET")?;
12286 self.consume_optional_rows_keyword();
12287 head.offset = Some(off);
12288 saw_offset = true;
12289 continue;
12290 }
12291 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12292 // the SQL-standard alias for LIMIT. PG accepts both
12293 // spellings interchangeably; pg_dump emits FETCH FIRST in
12294 // newer versions. We map it onto `head.limit` so the
12295 // engine path is unified.
12296 if !saw_limit
12297 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12298 if s.eq_ignore_ascii_case("fetch"))
12299 {
12300 self.advance(); // FETCH
12301 // `FIRST` or `NEXT` (both legal per SQL standard).
12302 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12303 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12304 {
12305 self.advance();
12306 }
12307 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12308 // implicit 1 — but we always consume one if present).
12309 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12310 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12311 {
12312 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12313 crate::ast::LimitExpr::Literal(1)
12314 } else {
12315 self.parse_limit_expr("FETCH FIRST")?
12316 };
12317 // Eat `ROW` / `ROWS` if not already consumed above.
12318 self.consume_optional_rows_keyword();
12319 // Optional `ONLY` (the spec form) — or the SQL:2008
12320 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12321 // now honours WITH TIES by extending past the LIMIT
12322 // truncation point through every row that shares the
12323 // last-kept row's ORDER BY key.
12324 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12325 if s.eq_ignore_ascii_case("only"))
12326 {
12327 self.advance();
12328 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12329 if s.eq_ignore_ascii_case("with"))
12330 {
12331 self.advance(); // WITH
12332 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12333 if s.eq_ignore_ascii_case("ties"))
12334 {
12335 self.advance();
12336 head.limit_with_ties = true;
12337 }
12338 }
12339 head.limit = Some(count);
12340 saw_limit = true;
12341 continue;
12342 }
12343 break;
12344 }
12345 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12346 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12347 // [ OF table_name [, …] ]
12348 // [ NOWAIT | SKIP LOCKED ]
12349 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12350 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12351 // SELECT already returns a consistent snapshot — so these
12352 // are accept-and-discard: the parser absorbs them so
12353 // mailrs / Rails / Django code paths that emit `SELECT
12354 // … FOR UPDATE` for advisory pessimistic locking load
12355 // without a parser error. The on-disk locking model is
12356 // unchanged; callers that rely on FOR UPDATE for read-
12357 // through-write ordering still get the right answer
12358 // because SPG serialises writes anyway.
12359 head.locking = self
12360 .consume_optional_for_lock_clauses()
12361 .map(alloc::boxed::Box::new);
12362 Ok(())
12363 }
12364
12365 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12366 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12367 /// LOCKED ]` trailers. Each clause is fully accepted and
12368 /// discarded — SPG's single-writer model already satisfies the
12369 /// callers' implicit ordering requirement. Stops at the first
12370 /// token that isn't `FOR`.
12371 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12372 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12373 // not discarded. PG keeps the strongest of several clauses; the
12374 // policy of the last one wins, which is what this loop records.
12375 let mut seen: Option<crate::ast::LockingClause> = None;
12376 while matches!(self.peek(), Token::For) {
12377 // v7.37.14 (A2.5-stub) — record that this query asked
12378 // for a row lock the parser is about to silently
12379 // discard. Operators surface the count via
12380 // `spg_sql::silent_for_update_count()` so they can
12381 // gauge how much of the workload depends on advisory
12382 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12383 // before v7.37.15's per-row tuple locking lands.
12384 crate::record_silent_for_update_clause();
12385 self.advance(); // FOR
12386 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12387 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12388 let mut no_key = false;
12389 let mut key = false;
12390 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12391 if s.eq_ignore_ascii_case("no"))
12392 {
12393 self.advance(); // NO
12394 no_key = true;
12395 // The next ident should be KEY but be generous;
12396 // anything followed by UPDATE/SHARE is accepted.
12397 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12398 if s.eq_ignore_ascii_case("key"))
12399 {
12400 self.advance(); // KEY
12401 }
12402 }
12403 // `KEY` prefix (PG `FOR KEY SHARE`).
12404 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12405 if s.eq_ignore_ascii_case("key"))
12406 {
12407 self.advance(); // KEY
12408 key = true;
12409 }
12410 // Lock-strength keyword: UPDATE / SHARE. Required, but
12411 // we're lenient — an unexpected token here just bails
12412 // (we already consumed FOR; caller's downstream
12413 // dispatch will error if anything actually depends on
12414 // the trailing tokens).
12415 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12416 if s.eq_ignore_ascii_case("update"));
12417 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12419 {
12420 self.advance();
12421 use crate::ast::LockStrength as LS;
12422 let strength = match (is_update, no_key, key) {
12423 (true, true, _) => LS::NoKeyUpdate,
12424 (true, _, _) => LS::Update,
12425 (false, _, true) => LS::KeyShare,
12426 (false, _, _) => LS::Share,
12427 };
12428 seen = Some(crate::ast::LockingClause {
12429 strength,
12430 of_tables: alloc::vec::Vec::new(),
12431 policy: crate::ast::LockWait::Wait,
12432 });
12433 } else {
12434 // FOR by itself (or `FOR KEY` with nothing after) —
12435 // give up on the lock-clause path. We've already
12436 // advanced past FOR; further attempts to parse
12437 // here would clobber state.
12438 return seen;
12439 }
12440 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12441 // joining and locking only a subset of tables.
12442 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12443 if s.eq_ignore_ascii_case("of"))
12444 {
12445 self.advance(); // OF
12446 #[allow(clippy::while_let_loop)]
12447 loop {
12448 match self.peek() {
12449 Token::Ident(_) | Token::QuotedIdent(_) => {
12450 // v7.39 (round 294) — the name is CAPTURED now: PG
12451 // validates it against the FROM clause, and an
12452 // uncaptured list silently means "lock everything".
12453 let mut nm = match self.advance() {
12454 Token::Ident(n) | Token::QuotedIdent(n) => n,
12455 _ => alloc::string::String::new(),
12456 };
12457 // Optional schema-qualified `schema.table`.
12458 if matches!(self.peek(), Token::Dot) {
12459 self.advance();
12460 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12461 {
12462 self.advance();
12463 nm = n;
12464 }
12465 }
12466 if let Some(c) = seen.as_mut() {
12467 c.of_tables.push(nm);
12468 }
12469 }
12470 _ => break,
12471 }
12472 if matches!(self.peek(), Token::Comma) {
12473 self.advance();
12474 } else {
12475 break;
12476 }
12477 }
12478 }
12479 // Optional `NOWAIT` | `SKIP LOCKED`.
12480 match self.peek().clone() {
12481 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12482 self.advance();
12483 if let Some(c) = seen.as_mut() {
12484 c.policy = crate::ast::LockWait::NoWait;
12485 }
12486 }
12487 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12488 self.advance(); // SKIP
12489 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12490 if s.eq_ignore_ascii_case("locked"))
12491 {
12492 self.advance(); // LOCKED
12493 if let Some(c) = seen.as_mut() {
12494 c.policy = crate::ast::LockWait::SkipLocked;
12495 }
12496 }
12497 }
12498 _ => {}
12499 }
12500 // Loop: PG allows multiple FOR clauses chained.
12501 }
12502 seen
12503 }
12504
12505 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12506 /// Bind value gets resolved during prepared-statement Execute;
12507 /// the Pratt expression parser would over-accept here (e.g.
12508 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12509 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12510 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12511 /// when one was consumed; caller skips the regular
12512 /// limit-value parse and leaves `head.limit` at None.
12513 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12514 if matches!(self.peek(), Token::Null) {
12515 self.advance();
12516 return true;
12517 }
12518 if matches!(self.peek(), Token::All) {
12519 self.advance();
12520 return true;
12521 }
12522 false
12523 }
12524
12525 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12526 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12527 /// SQL-standard shape. No-op when missing.
12528 fn consume_optional_rows_keyword(&mut self) {
12529 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12530 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12531 {
12532 self.advance();
12533 }
12534 }
12535
12536 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12537 ///
12538 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12539 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12540 /// constant, which is why that spelling keeps the token path below.
12541 ///
12542 /// Constants are folded here rather than carried into the tree: the
12543 /// 15+ execution paths that read the row count go through
12544 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12545 /// means "no limit". A clause the engine could not resolve would
12546 /// therefore return the WHOLE table instead of failing. Folding at
12547 /// parse time keeps that impossible; a non-constant clause is still
12548 /// a clean error (recorded residual — closing it wants a resolution
12549 /// pre-pass on the simple-query path, where `substitute_placeholders`
12550 /// does not run).
12551 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12552 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12553 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12554 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12555 // ONLY` both work (its grammar takes a c_expr). Both measured
12556 // against PG 18.4 in round 305.
12557 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12558 return self.parse_limit_constant(label);
12559 }
12560 // One pass, no rewind: `advance()` takes each token by
12561 // `mem::replace`, so a consumed token reads back as Eof and this
12562 // parser cannot backtrack. Everything — bare literal included —
12563 // is therefore folded from the parsed expression rather than
12564 // re-read from the token stream.
12565 let start = self.pos;
12566 let e = self.parse_expr(0)?;
12567 if let crate::ast::Expr::Placeholder(n) = e {
12568 return Ok(crate::ast::LimitExpr::Placeholder(n));
12569 }
12570 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12571 match fold_limit_constant(&e) {
12572 Some(Ok(v)) if v < 0 => Err(ParseError {
12573 message: alloc::format!("{neg_label} must not be negative"),
12574 token_pos: start,
12575 }),
12576 Some(Ok(v)) => u32::try_from(v)
12577 .map(crate::ast::LimitExpr::Literal)
12578 .map_err(|_| ParseError {
12579 message: alloc::format!("{label} value too large: {v}"),
12580 token_pos: start,
12581 }),
12582 Some(Err(message)) => Err(ParseError {
12583 message: message.replace("{L}", neg_label),
12584 token_pos: start,
12585 }),
12586 // v7.39 (round 305, V23) — not foldable at parse time
12587 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12588 // expression; the engine evaluates it once before dispatch.
12589 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12590 }
12591 }
12592
12593 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12594 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12595 // coercion rules, not just an integer token: a NUMERIC rounds half
12596 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12597 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12598 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12599 // content, failing as an input-syntax error on the value. General
12600 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12601 // they need an Expr-carrying LimitExpr variant.
12602 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12603 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12604 message,
12605 token_pos: pos,
12606 };
12607 match self.advance() {
12608 Token::Integer(n) if n >= 0 => u32::try_from(n)
12609 .map(crate::ast::LimitExpr::Literal)
12610 .map_err(|_| ParseError {
12611 message: alloc::format!("{label} value too large: {n}"),
12612 token_pos: self.consumed_pos(),
12613 }),
12614 Token::Integer(_) => Err(err_at(
12615 alloc::format!("{neg_label} must not be negative"),
12616 self.pos.saturating_sub(1),
12617 )),
12618 Token::Numeric(t) => {
12619 let pos = self.pos.saturating_sub(1);
12620 let v: f64 = t.parse().map_err(|_| {
12621 err_at(
12622 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12623 pos,
12624 )
12625 })?;
12626 if v < 0.0 {
12627 return Err(err_at(
12628 alloc::format!("{neg_label} must not be negative"),
12629 pos,
12630 ));
12631 }
12632 // Round half away from zero — PG's numeric→bigint cast.
12633 // (no_std: no f64::round; v is non-negative, so truncating
12634 // v + 0.5 is the same thing.)
12635 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12636 let rounded = (v + 0.5) as u64;
12637 u32::try_from(rounded)
12638 .map(crate::ast::LimitExpr::Literal)
12639 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12640 }
12641 Token::Minus => {
12642 let pos = self.pos.saturating_sub(1);
12643 match self.peek() {
12644 Token::Integer(_) | Token::Numeric(_) => {
12645 self.advance();
12646 Err(err_at(
12647 alloc::format!("{neg_label} must not be negative"),
12648 pos,
12649 ))
12650 }
12651 other => Err(err_at(
12652 alloc::format!(
12653 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12654 ),
12655 pos,
12656 )),
12657 }
12658 }
12659 Token::String(t) => {
12660 let pos = self.pos.saturating_sub(1);
12661 match t.trim().parse::<i64>() {
12662 Ok(n) if n < 0 => Err(err_at(
12663 alloc::format!("{neg_label} must not be negative"),
12664 pos,
12665 )),
12666 Ok(n) => u32::try_from(n)
12667 .map(crate::ast::LimitExpr::Literal)
12668 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12669 Err(_) => Err(err_at(
12670 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12671 pos,
12672 )),
12673 }
12674 }
12675 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12676 other => Err(ParseError {
12677 message: alloc::format!(
12678 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12679 ),
12680 token_pos: self.consumed_pos(),
12681 }),
12682 }
12683 }
12684
12685 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12686 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12687 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12688 /// `parse_select_stmt` is responsible for filling those in.
12689 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12690 /// call in the expression tree to the per-set integer bitmask
12691 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12692 /// is dropped in this grouping set). Runs during the ROLLUP /
12693 /// CUBE / GROUPING SETS expansion, where the set is known.
12694 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12695 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12696 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12697 if let Expr::FunctionCall { name, .. } = expr
12698 && name.eq_ignore_ascii_case("grouping")
12699 {
12700 if !out.iter().any(|e| e == expr) {
12701 out.push(expr.clone());
12702 }
12703 return;
12704 }
12705 match expr {
12706 Expr::Binary { lhs, rhs, .. } => {
12707 Self::collect_grouping_calls(lhs, out);
12708 Self::collect_grouping_calls(rhs, out);
12709 }
12710 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12711 Self::collect_grouping_calls(expr, out)
12712 }
12713 Expr::FunctionCall { args, .. } => {
12714 for a in args {
12715 Self::collect_grouping_calls(a, out);
12716 }
12717 }
12718 Expr::Case {
12719 operand,
12720 branches,
12721 else_branch,
12722 } => {
12723 if let Some(o) = operand {
12724 Self::collect_grouping_calls(o, out);
12725 }
12726 for (c, v) in branches {
12727 Self::collect_grouping_calls(c, out);
12728 Self::collect_grouping_calls(v, out);
12729 }
12730 if let Some(x) = else_branch {
12731 Self::collect_grouping_calls(x, out);
12732 }
12733 }
12734 _ => {}
12735 }
12736 }
12737
12738 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12739 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12740 /// `__grp_ord_k` (injected per grouping-set branch).
12741 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12742 if let Expr::FunctionCall { name, .. } = expr
12743 && name.eq_ignore_ascii_case("grouping")
12744 {
12745 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12746 *expr = Expr::Column(crate::ast::ColumnName {
12747 qualifier: None,
12748 name: alloc::format!("__grp_ord_{k}"),
12749 });
12750 }
12751 return;
12752 }
12753 match expr {
12754 Expr::Binary { lhs, rhs, .. } => {
12755 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12756 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12757 }
12758 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12759 Self::rewrite_grouping_to_col(expr, grp_exprs)
12760 }
12761 Expr::FunctionCall { args, .. } => {
12762 for a in args {
12763 Self::rewrite_grouping_to_col(a, grp_exprs);
12764 }
12765 }
12766 Expr::Case {
12767 operand,
12768 branches,
12769 else_branch,
12770 } => {
12771 if let Some(o) = operand {
12772 Self::rewrite_grouping_to_col(o, grp_exprs);
12773 }
12774 for (c, v) in branches {
12775 Self::rewrite_grouping_to_col(c, grp_exprs);
12776 Self::rewrite_grouping_to_col(v, grp_exprs);
12777 }
12778 if let Some(x) = else_branch {
12779 Self::rewrite_grouping_to_col(x, grp_exprs);
12780 }
12781 }
12782 _ => {}
12783 }
12784 }
12785
12786 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12787 /// as the list of key sets it contributes. A bare expression is one
12788 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12789 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12790 /// the concatenation of its items' sets, where an item is itself an
12791 /// element, a parenthesized key list, or the empty set `()`. A
12792 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12793 /// move together.
12794 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12795 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12796 // ROLLUP ( … ) / CUBE ( … )
12797 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12798 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12799 {
12800 let is_cube = is_kw(self.peek(), "cube");
12801 self.advance(); // ROLLUP / CUBE
12802 self.advance(); // (
12803 let mut units: Vec<Vec<Expr>> = Vec::new();
12804 loop {
12805 if matches!(self.peek(), Token::LParen) {
12806 // Composite unit: (a, b) rolls up as one.
12807 self.advance();
12808 let mut unit = Vec::new();
12809 if !matches!(self.peek(), Token::RParen) {
12810 loop {
12811 unit.push(self.parse_expr(0)?);
12812 match self.peek() {
12813 Token::Comma => {
12814 self.advance();
12815 }
12816 Token::RParen => break,
12817 other => {
12818 return Err(self.err(format!(
12819 "expected ',' or ')' in grouping unit, got {other:?}"
12820 )));
12821 }
12822 }
12823 }
12824 }
12825 self.advance(); // )
12826 units.push(unit);
12827 } else {
12828 units.push(alloc::vec![self.parse_expr(0)?]);
12829 }
12830 match self.peek() {
12831 Token::Comma => {
12832 self.advance();
12833 }
12834 Token::RParen => break,
12835 other => {
12836 return Err(self.err(format!(
12837 "expected ',' or ')' in grouping list, got {other:?}"
12838 )));
12839 }
12840 }
12841 }
12842 self.advance(); // )
12843 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12844 units
12845 .iter()
12846 .zip(unit_sel.iter())
12847 .filter(|(_, keep)| **keep)
12848 .flat_map(|(u, _)| u.iter().cloned())
12849 .collect()
12850 };
12851 let n = units.len();
12852 if is_cube {
12853 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12854 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12855 .collect();
12856 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12857 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12858 }
12859 return Ok((0..=n)
12860 .rev()
12861 .map(|keep| {
12862 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12863 flatten(&sel)
12864 })
12865 .collect());
12866 }
12867 // GROUPING SETS ( item [, item]* )
12868 if is_kw(self.peek(), "grouping")
12869 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12870 {
12871 self.advance(); // GROUPING
12872 self.advance(); // SETS
12873 if !matches!(self.peek(), Token::LParen) {
12874 return Err(self.err(format!(
12875 "expected '(' after GROUPING SETS, got {:?}",
12876 self.peek()
12877 )));
12878 }
12879 self.advance(); // outer (
12880 let mut sets: Vec<Vec<Expr>> = Vec::new();
12881 loop {
12882 if matches!(self.peek(), Token::LParen) {
12883 // A parenthesized key list (or the empty set).
12884 self.advance();
12885 let mut set = Vec::new();
12886 if !matches!(self.peek(), Token::RParen) {
12887 loop {
12888 set.push(self.parse_expr(0)?);
12889 match self.peek() {
12890 Token::Comma => {
12891 self.advance();
12892 }
12893 Token::RParen => break,
12894 other => {
12895 return Err(self.err(format!(
12896 "expected ',' or ')' in grouping set, got {other:?}"
12897 )));
12898 }
12899 }
12900 }
12901 }
12902 self.advance(); // )
12903 sets.push(set);
12904 } else {
12905 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12906 // bare expression.
12907 sets.extend(self.parse_grouping_element()?);
12908 }
12909 match self.peek() {
12910 Token::Comma => {
12911 self.advance();
12912 }
12913 Token::RParen => break,
12914 other => {
12915 return Err(self.err(format!(
12916 "expected ',' or ')' after a grouping set, got {other:?}"
12917 )));
12918 }
12919 }
12920 }
12921 self.advance(); // outer )
12922 return Ok(sets);
12923 }
12924 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12925 }
12926
12927 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12928 // v7.38 (read01) — a reference to a key that is dropped in this grouping
12929 // set evaluates to NULL, at any depth. Previously only a *top-level*
12930 // select item equal to a dropped key was nullified, so a key nested in
12931 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12932 // column and failed to resolve against the set's synthetic schema.
12933 if dropped.iter().any(|d| d == expr) {
12934 *expr = Expr::Literal(Literal::Null);
12935 return;
12936 }
12937 if let Expr::FunctionCall { name, args } = expr
12938 && name.eq_ignore_ascii_case("grouping")
12939 {
12940 let mut mask: i64 = 0;
12941 for a in args.iter() {
12942 mask <<= 1;
12943 if dropped.iter().any(|d| d == a) {
12944 mask |= 1;
12945 }
12946 }
12947 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12948 // literal: a bare integer in a select item is indistinguishable
12949 // from a positional reference once `ORDER BY 1` substitutes the
12950 // item back in, and the round-232 position check then read the
12951 // mask value as an out-of-range position. The cast changes
12952 // nothing semantically (grouping() is integer).
12953 *expr = Expr::Cast {
12954 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12955 target: crate::ast::CastTarget::Int,
12956 };
12957 return;
12958 }
12959 // Generic recursion over the common expression shapes the
12960 // SELECT list uses; anything without child expressions is
12961 // left alone.
12962 match expr {
12963 Expr::FunctionCall { args, .. } => {
12964 for a in args {
12965 Self::substitute_grouping_calls(a, dropped);
12966 }
12967 }
12968 Expr::Binary { lhs, rhs, .. } => {
12969 Self::substitute_grouping_calls(lhs, dropped);
12970 Self::substitute_grouping_calls(rhs, dropped);
12971 }
12972 Expr::Unary { expr: inner, .. } => {
12973 Self::substitute_grouping_calls(inner, dropped);
12974 }
12975 Expr::Cast { expr: inner, .. } => {
12976 Self::substitute_grouping_calls(inner, dropped);
12977 }
12978 Expr::Case {
12979 operand,
12980 branches,
12981 else_branch,
12982 } => {
12983 if let Some(op) = operand {
12984 Self::substitute_grouping_calls(op, dropped);
12985 }
12986 for (w, t) in branches {
12987 Self::substitute_grouping_calls(w, dropped);
12988 Self::substitute_grouping_calls(t, dropped);
12989 }
12990 if let Some(e) = else_branch {
12991 Self::substitute_grouping_calls(e, dropped);
12992 }
12993 }
12994 // v7.38 (read01) — recurse into the remaining child-bearing shapes
12995 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
12996 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
12997 // …` is the canonical rollup-total label idiom).
12998 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
12999 Expr::Like { expr, pattern, .. } => {
13000 Self::substitute_grouping_calls(expr, dropped);
13001 Self::substitute_grouping_calls(pattern, dropped);
13002 }
13003 Expr::InList { expr, list, .. } => {
13004 Self::substitute_grouping_calls(expr, dropped);
13005 for item in list {
13006 Self::substitute_grouping_calls(item, dropped);
13007 }
13008 }
13009 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13010 Expr::Array(items) => {
13011 for item in items {
13012 Self::substitute_grouping_calls(item, dropped);
13013 }
13014 }
13015 Expr::ArraySubscript { target, index } => {
13016 Self::substitute_grouping_calls(target, dropped);
13017 Self::substitute_grouping_calls(index, dropped);
13018 }
13019 Expr::ArraySlice { target, lo, hi } => {
13020 Self::substitute_grouping_calls(target, dropped);
13021 if let Some(lo) = lo {
13022 Self::substitute_grouping_calls(lo, dropped);
13023 }
13024 if let Some(hi) = hi {
13025 Self::substitute_grouping_calls(hi, dropped);
13026 }
13027 }
13028 Expr::AnyAll { expr, array, .. } => {
13029 Self::substitute_grouping_calls(expr, dropped);
13030 Self::substitute_grouping_calls(array, dropped);
13031 }
13032 _ => {}
13033 }
13034 }
13035
13036 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13037 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13038 // group: `( <select chain> )` usable anywhere a query block
13039 // is (head or peer of an outer chain). The group's own
13040 // unions ride the returned SelectStatement; the executor's
13041 // nested-peer recursion runs them.
13042 if matches!(self.peek(), Token::LParen)
13043 && matches!(
13044 self.tokens.get(self.pos + 1),
13045 Some(Token::Select | Token::LParen | Token::Values)
13046 )
13047 {
13048 self.advance(); // (
13049 self.enter_nested()?;
13050 // v7.37 D.20 — a group whose head is a VALUES list:
13051 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13052 // otherwise recurse into a nested SELECT/group head.
13053 let mut head = (if matches!(self.peek(), Token::Values) {
13054 self.advance(); // VALUES
13055 self.parse_values_rows_body()
13056 } else {
13057 self.parse_bare_select()
13058 })
13059 .and_then(|mut h| {
13060 self.parse_setop_chain_into(&mut h)?;
13061 Ok(h)
13062 });
13063 self.nest_depth -= 1;
13064 let mut head = match &mut head {
13065 Ok(h) => core::mem::take(h),
13066 Err(_) => return head,
13067 };
13068 // v7.37.17 (17.6 siblings) — group-internal tail:
13069 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13070 // group head, then wrap the group as a derived table
13071 // (SELECT * FROM (group)) so the outer chain / outer
13072 // tail can't clobber the group's own ordering or limit.
13073 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13074 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13075 if s.eq_ignore_ascii_case("fetch"));
13076 if has_tail {
13077 self.parse_select_tail_into(&mut head)?;
13078 head = SelectStatement {
13079 locking: None,
13080 ctes: Vec::new(),
13081 distinct: false,
13082 distinct_on: Vec::new(),
13083 items: alloc::vec![SelectItem::Wildcard],
13084 from: Some(FromClause {
13085 primary: TableRef {
13086 name: "subquery".to_string(),
13087 alias: None,
13088 only: false,
13089 as_of_segment: None,
13090 unnest_expr: None,
13091 unnest_column_aliases: Vec::new(),
13092 with_ordinality: false,
13093 generate_series_args: None,
13094 lateral_subquery: Some(Box::new(head)),
13095 jsonb_each_text_arg: None,
13096 table_fn_call: None,
13097 rows_from: None,
13098 json_table: None,
13099 scalar_fn_item: false,
13100 },
13101 joins: Vec::new(),
13102 }),
13103 where_: None,
13104 group_by: None,
13105 group_by_all: false,
13106 having: None,
13107 unions: Vec::new(),
13108 order_by: Vec::new(),
13109 limit: None,
13110 offset: None,
13111 limit_with_ties: false,
13112 window_check_exprs: Vec::new(),
13113 };
13114 }
13115 if !matches!(self.peek(), Token::RParen) {
13116 return Err(self.err(format!(
13117 "expected ')' after parenthesized query group, got {:?}",
13118 self.peek()
13119 )));
13120 }
13121 self.advance();
13122 return Ok(head);
13123 }
13124 // `TABLE name` shorthand as a query block — valid anywhere
13125 // a SELECT head is (set-op peers included).
13126 if matches!(self.peek(), Token::Table)
13127 && matches!(
13128 self.tokens.get(self.pos + 1),
13129 Some(Token::Ident(_) | Token::QuotedIdent(_))
13130 )
13131 {
13132 return self.parse_table_shorthand();
13133 }
13134 if !matches!(self.peek(), Token::Select) {
13135 return Err(self.err(format!(
13136 "expected SELECT to start a query block, got {:?}",
13137 self.peek()
13138 )));
13139 }
13140 self.advance();
13141 let distinct = if matches!(self.peek(), Token::Distinct) {
13142 self.advance();
13143 true
13144 } else {
13145 false
13146 };
13147 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13148 // keep the first row (per ORDER BY) of each group the
13149 // expressions define. Django's .distinct('field') shape.
13150 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13151 self.advance(); // ON
13152 if !matches!(self.peek(), Token::LParen) {
13153 return Err(self.err(format!(
13154 "expected '(' after DISTINCT ON, got {:?}",
13155 self.peek()
13156 )));
13157 }
13158 self.advance();
13159 let mut exprs = Vec::new();
13160 loop {
13161 exprs.push(self.parse_expr(0)?);
13162 match self.peek() {
13163 Token::Comma => {
13164 self.advance();
13165 }
13166 Token::RParen => break,
13167 other => {
13168 return Err(self.err(format!(
13169 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13170 )));
13171 }
13172 }
13173 }
13174 self.advance(); // )
13175 exprs
13176 } else {
13177 Vec::new()
13178 };
13179 let mut items = self.parse_select_list()?;
13180 // Scope the TABLESAMPLE lowering channel to this SELECT:
13181 // stash whatever an enclosing select accumulated, collect
13182 // our own FROM's predicates, restore after the combine.
13183 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13184 let mut from = if matches!(self.peek(), Token::From) {
13185 self.advance();
13186 Some(self.parse_from_clause()?)
13187 } else {
13188 None
13189 };
13190 // v7.37 D.22 — a set-returning function in the projection with no FROM
13191 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13192 // rows. Move the first SRF projection item to a FROM-position derived
13193 // table and replace it in the projection with a reference to its output
13194 // column; sibling scalar columns repeat per SRF row. PG names the output
13195 // column after the function (or its AS alias). Reuses the FROM-SRF
13196 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13197 // works via the targetlist-SRF path.
13198 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13199 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13200 // is exactly what the function's own row shape already is. Anywhere else
13201 // (per outer row, or beside other items) it would need a real record-typed
13202 // projection, so it says so rather than answering something else.
13203 if let [
13204 SelectItem::Expr {
13205 expr: Expr::FunctionCall { name, args },
13206 ..
13207 },
13208 ] = items.as_slice()
13209 && name == "__record_expand"
13210 {
13211 let Some(Expr::FunctionCall {
13212 name: inner_name,
13213 args: inner_args,
13214 }) = args.first()
13215 else {
13216 return Err(self.err(
13217 "(<expr>).* expands a function's record — it needs a function call".into(),
13218 ));
13219 };
13220 if from.is_some() {
13221 return Err(self.err(
13222 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13223 .into(),
13224 ));
13225 }
13226 let fn_ref = TableRef {
13227 name: inner_name.clone(),
13228 alias: None,
13229 only: false,
13230 as_of_segment: None,
13231 unnest_expr: None,
13232 unnest_column_aliases: Vec::new(),
13233 with_ordinality: false,
13234 generate_series_args: None,
13235 lateral_subquery: None,
13236 jsonb_each_text_arg: None,
13237 table_fn_call: Some(Box::new((
13238 inner_name.to_ascii_lowercase(),
13239 inner_args.clone(),
13240 ))),
13241 rows_from: None,
13242 json_table: None,
13243 scalar_fn_item: false,
13244 };
13245 items = alloc::vec![SelectItem::Wildcard];
13246 from = Some(FromClause {
13247 primary: fn_ref,
13248 joins: Vec::new(),
13249 });
13250 }
13251 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13252 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13253 // record's fields takes the catalog. It becomes a LATERAL of the same
13254 // function plus one item per declared column — the machinery rounds 65
13255 // and 69 already built.
13256 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13257 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13258 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13259 // express, since the lifted one becomes a scan and the other would
13260 // expand per its rows (a cross product, not a zip). So when the
13261 // projection holds more than one top-level function call, the lift steps
13262 // aside and the engine's target-list expansion takes the whole list.
13263 let fn_call_items = items
13264 .iter()
13265 .filter(|it| {
13266 matches!(
13267 it,
13268 SelectItem::Expr {
13269 expr: Expr::FunctionCall { .. },
13270 ..
13271 }
13272 )
13273 })
13274 .count();
13275 if from.is_none() && fn_call_items <= 1 {
13276 let mut found: Option<(usize, TableRef, String)> = None;
13277 for (i, item) in items.iter().enumerate() {
13278 if let SelectItem::Expr {
13279 expr: Expr::FunctionCall { name, args },
13280 alias,
13281 } = item
13282 {
13283 let lname = name.to_ascii_lowercase();
13284 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13285 let (unnest, gs) = match lname.as_str() {
13286 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13287 "generate_series" if (2..=3).contains(&args.len()) => {
13288 (None, Some(args.clone()))
13289 }
13290 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13291 // no-FROM projection yields the 1-based subscripts, i.e.
13292 // generate_series(1, array_length(arr, dim)); an invalid
13293 // dimension makes array_length NULL → 0 rows, as in PG.
13294 "generate_subscripts" if args.len() == 2 => (
13295 None,
13296 Some(alloc::vec![
13297 Expr::Literal(Literal::Integer(1)),
13298 Expr::FunctionCall {
13299 name: "array_length".to_string(),
13300 args: args.clone(),
13301 },
13302 ]),
13303 ),
13304 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13305 // in a no-FROM projection unnest their *_to_array form.
13306 "string_to_table" | "regexp_split_to_table" => {
13307 let array_fn = if lname == "string_to_table" {
13308 "string_to_array"
13309 } else {
13310 "regexp_split_to_array"
13311 };
13312 (
13313 Some(Box::new(Expr::FunctionCall {
13314 name: array_fn.to_string(),
13315 args: args.clone(),
13316 })),
13317 None,
13318 )
13319 }
13320 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13321 // a no-FROM projection expand per element. The scalar form
13322 // returns the elements as a TEXT array, so unnest over the
13323 // same call materialises one row each (same rewrite the
13324 // FROM-clause form uses).
13325 "jsonb_array_elements"
13326 | "json_array_elements"
13327 | "jsonb_array_elements_text"
13328 | "json_array_elements_text"
13329 if args.len() == 1 =>
13330 {
13331 (
13332 Some(Box::new(Expr::FunctionCall {
13333 name: lname.clone(),
13334 args: args.clone(),
13335 })),
13336 None,
13337 )
13338 }
13339 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13340 // in a no-FROM projection expands per match (scalar form
13341 // returns the matches as a TEXT array → unnest).
13342 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13343 Some(Box::new(Expr::FunctionCall {
13344 name: lname.clone(),
13345 args: args.clone(),
13346 })),
13347 None,
13348 ),
13349 _ => continue,
13350 };
13351 found = Some((
13352 i,
13353 TableRef {
13354 name: colname.clone(),
13355 alias: Some(colname.clone()),
13356 only: false,
13357 as_of_segment: None,
13358 unnest_expr: unnest,
13359 unnest_column_aliases: alloc::vec![colname.clone()],
13360 with_ordinality: false,
13361 generate_series_args: gs,
13362 lateral_subquery: None,
13363 jsonb_each_text_arg: None,
13364 table_fn_call: None,
13365 rows_from: None,
13366 json_table: None,
13367 scalar_fn_item: false,
13368 },
13369 colname,
13370 ));
13371 break;
13372 }
13373 }
13374 if let Some((idx, tref, colname)) = found {
13375 from = Some(FromClause {
13376 primary: tref,
13377 joins: Vec::new(),
13378 });
13379 items[idx] = SelectItem::Expr {
13380 expr: Expr::Column(ColumnName {
13381 qualifier: None,
13382 name: colname.clone(),
13383 }),
13384 alias: Some(colname),
13385 };
13386 }
13387 }
13388 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13389 let where_ = if matches!(self.peek(), Token::Where) {
13390 self.advance();
13391 Some(self.parse_expr(0)?)
13392 } else {
13393 None
13394 };
13395 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13396 Some(match acc {
13397 Some(w) => Expr::Binary {
13398 lhs: Box::new(pred),
13399 op: crate::ast::BinOp::And,
13400 rhs: Box::new(w),
13401 },
13402 None => pred,
13403 })
13404 });
13405 self.pending_sample_preds = enclosing_sample_preds;
13406 let mut group_by_all = false;
13407 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13408 // share one expansion: `grouping_sets` lists the key subsets
13409 // (first = primary, assigned to stmt.group_by; the rest
13410 // become UNION ALL peers), `grouping_universe` is the full
13411 // key list used to compute each peer's dropped keys.
13412 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13413 let mut grouping_universe: Vec<Expr> = Vec::new();
13414 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13415 // A BOOL, not the key list: this frame is the statement parser's, and
13416 // round 430 measured that a `Vec` local here is enough on its own to
13417 // tip the 512 KiB nesting guard. The keys are recoverable from
13418 // `grouping_universe`, which a rollup fills with exactly them.
13419 let mut mysql_rollup = false;
13420 let group_by = if matches!(self.peek(), Token::Group) {
13421 self.advance();
13422 if !self.peek_is_by() {
13423 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13424 }
13425 self.advance();
13426 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13427 // every non-aggregate SELECT-list item later.
13428 if matches!(self.peek(), Token::All) {
13429 self.advance();
13430 group_by_all = true;
13431 None
13432 } else {
13433 // v7.39 (round 242) — PG's general grouping-element grammar:
13434 // GROUP BY [DISTINCT] element [, element]*, where an element
13435 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13436 // SETS (…) — mixed freely. Each element yields a list of
13437 // key sets; the query's grouping sets are the CARTESIAN
13438 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13439 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13440 // content. ROLLUP/CUBE members may be composite
13441 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13442 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13443 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13444 // clause.
13445 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13446 self.advance();
13447 true
13448 } else {
13449 false
13450 };
13451 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13452 loop {
13453 element_sets.push(self.parse_grouping_element()?);
13454 if matches!(self.peek(), Token::Comma) {
13455 self.advance();
13456 } else {
13457 break;
13458 }
13459 }
13460 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13461 for el in &element_sets {
13462 let mut next: Vec<Vec<Expr>> = Vec::new();
13463 for base in &total {
13464 for set in el {
13465 let mut merged = base.clone();
13466 for k in set {
13467 if !merged.iter().any(|m| m == k) {
13468 merged.push(k.clone());
13469 }
13470 }
13471 next.push(merged);
13472 }
13473 }
13474 total = next;
13475 }
13476 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13477 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13478 // The keys and the aggregates come out identical; the ROW
13479 // ORDER does not, and that is the part a report depends on.
13480 // MySQL interleaves each group's subtotal right after its
13481 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13482 // where the union-of-grouping-sets expansion emits every
13483 // leaf first and then every subtotal. MariaDB REFUSES an
13484 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13485 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13486 // agree on the order and disagree only on whether ORDER BY
13487 // is allowed (MySQL allows it; SPG allows it too, since
13488 // refusing would break the clients that can write it).
13489 if self.mysql_dialect
13490 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13491 && matches!(
13492 self.tokens.get(self.pos + 1),
13493 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13494 )
13495 {
13496 self.advance(); // WITH
13497 self.advance(); // ROLLUP
13498 let keys = total.into_iter().next().unwrap_or_default();
13499 mysql_rollup = true;
13500 // n+1 prefixes, largest first — the same expansion
13501 // `ROLLUP (…)` produces.
13502 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13503 }
13504 if distinct_sets {
13505 let mut seen: Vec<Vec<String>> = Vec::new();
13506 total.retain(|set| {
13507 let mut key: Vec<String> =
13508 set.iter().map(|e| alloc::format!("{e}")).collect();
13509 key.sort();
13510 if seen.contains(&key) {
13511 false
13512 } else {
13513 seen.push(key);
13514 true
13515 }
13516 });
13517 }
13518 if total.len() > 1 {
13519 let mut universe: Vec<Expr> = Vec::new();
13520 for set in &total {
13521 for k in set {
13522 if !universe.iter().any(|u| u == k) {
13523 universe.push(k.clone());
13524 }
13525 }
13526 }
13527 grouping_universe = universe;
13528 let primary = total[0].clone();
13529 grouping_sets = total;
13530 Some(primary)
13531 } else {
13532 // One set (a plain GROUP BY list, or a single-set
13533 // spelling like GROUPING SETS ((a, b))). An EMPTY
13534 // single set — GROUPING SETS (()) — stays
13535 // `Some(vec![])`: the grand-total group, which must
13536 // run the aggregate path.
13537 Some(total.into_iter().next().unwrap_or_default())
13538 }
13539 }
13540 } else {
13541 None
13542 };
13543 let having = if matches!(self.peek(), Token::Having) {
13544 self.advance();
13545 Some(self.parse_expr(0)?)
13546 } else {
13547 None
13548 };
13549 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13550 // OVER w parsed to a marker above; inline each definition
13551 // into the referencing WindowFunction nodes.
13552 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13553 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13554 self.advance();
13555 loop {
13556 let wname = self.expect_ident_like()?;
13557 if !matches!(self.peek(), Token::As) {
13558 return Err(self.err(format!(
13559 "expected AS after WINDOW {wname}, got {:?}",
13560 self.peek()
13561 )));
13562 }
13563 self.advance();
13564 // v7.39 (round 229) — PG rejects a redefinition outright.
13565 if window_defs
13566 .iter()
13567 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13568 {
13569 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13570 }
13571 let def = self.parse_over_clause()?;
13572 // A definition may itself copy an earlier one
13573 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13574 // so resolve it against the defs already in scope. Same
13575 // copy rules as an `OVER (w1 …)` in the select list.
13576 let mut probe = Expr::WindowFunction {
13577 name: String::new(),
13578 args: Vec::new(),
13579 partition_by: def.0,
13580 order_by: def.1,
13581 frame: def.2,
13582 null_treatment: crate::ast::NullTreatment::Respect,
13583 filter: None,
13584 };
13585 Self::substitute_named_windows(&mut probe, &window_defs)
13586 .map_err(|m| self.err(m))?;
13587 let Expr::WindowFunction {
13588 partition_by,
13589 order_by,
13590 frame,
13591 ..
13592 } = probe
13593 else {
13594 unreachable!("probe is a WindowFunction")
13595 };
13596 window_defs.push((wname, (partition_by, order_by, frame)));
13597 if matches!(self.peek(), Token::Comma) {
13598 self.advance();
13599 continue;
13600 }
13601 break;
13602 }
13603 }
13604 // v7.39 (round 705) — which definitions did anything reference?
13605 // The ones nothing did used to be dropped here, unexamined, so
13606 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13607 // definition whether referenced or not. Their key expressions ride
13608 // out on the statement for the engine to resolve.
13609 let mut window_refs: Vec<String> = Vec::new();
13610 if !window_defs.is_empty() {
13611 for it in &items {
13612 if let SelectItem::Expr { expr, .. } = it {
13613 Self::collect_named_window_refs(expr, &mut window_refs);
13614 }
13615 }
13616 }
13617 let window_check_exprs: Vec<Expr> = window_defs
13618 .iter()
13619 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13620 .flat_map(|(_, (partition, order, _))| {
13621 partition
13622 .iter()
13623 .cloned()
13624 .chain(order.iter().map(|(e, _, _)| e.clone()))
13625 })
13626 .collect();
13627 if !window_defs.is_empty()
13628 || items
13629 .iter()
13630 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13631 {
13632 for it in &mut items {
13633 if let SelectItem::Expr { expr, .. } = it {
13634 Self::substitute_named_windows(expr, &window_defs)
13635 .map_err(|m| self.err(m))?;
13636 }
13637 }
13638 }
13639 // `GROUP BY 1` — positional keys substitute with the Nth
13640 // select item's expression (same contract ORDER BY has had
13641 // since v6.x). Out-of-range positions error.
13642 let group_by = match group_by {
13643 Some(mut keys) => {
13644 for k in &mut keys {
13645 if let Expr::Literal(Literal::Integer(n)) = k {
13646 let idx = *n;
13647 if idx < 1 || idx as usize > items.len() {
13648 return Err(self.err(alloc::format!(
13649 "GROUP BY position {idx} is not in select list"
13650 )));
13651 }
13652 match &items[(idx - 1) as usize] {
13653 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13654 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13655 return Err(self.err(alloc::format!(
13656 "GROUP BY position {idx} references a wildcard item"
13657 )));
13658 }
13659 }
13660 }
13661 }
13662 Some(keys)
13663 }
13664 None => None,
13665 };
13666 let mut stmt = SelectStatement {
13667 locking: None,
13668 ctes: Vec::new(),
13669 distinct,
13670 distinct_on,
13671 items,
13672 from,
13673 where_,
13674 group_by,
13675 group_by_all,
13676 having,
13677 unions: Vec::new(),
13678 order_by: Vec::new(),
13679 limit: None,
13680 offset: None,
13681 limit_with_ties: false,
13682 window_check_exprs,
13683 };
13684 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13685 // first set is the primary (already on stmt.group_by); each
13686 // further set becomes a UNION ALL peer with its dropped
13687 // keys (universe minus the set) replaced by NULL literals
13688 // in the peer's items and group_by. PG-legal: non-grouped
13689 // select items must be group keys or aggregates, so a
13690 // dropped key's occurrences in the projection are exactly
13691 // the ones to nullify.
13692 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13693 // over a plain GROUP BY (every argument must be a group key; the
13694 // mask is then 0) and rejects anything else with 42803. SPG's
13695 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13696 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13697 // function `grouping`".
13698 if grouping_sets.len() <= 1 {
13699 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13700 let mut calls: Vec<Expr> = Vec::new();
13701 for item in &stmt.items {
13702 if let SelectItem::Expr { expr, .. } = item {
13703 Self::collect_grouping_calls(expr, &mut calls);
13704 }
13705 }
13706 if let Some(h) = &stmt.having {
13707 Self::collect_grouping_calls(h, &mut calls);
13708 }
13709 for call in &calls {
13710 let Expr::FunctionCall { args, .. } = call else {
13711 continue;
13712 };
13713 for a in args {
13714 if !keys.iter().any(|k| k == a) {
13715 return Err(self.err(
13716 "arguments to GROUPING must be grouping expressions of the associated query level"
13717 .to_string(),
13718 ));
13719 }
13720 }
13721 }
13722 if !calls.is_empty() {
13723 for item in &mut stmt.items {
13724 if let SelectItem::Expr { expr, .. } = item {
13725 Self::substitute_grouping_calls(expr, &[]);
13726 }
13727 }
13728 if let Some(h) = &mut stmt.having {
13729 Self::substitute_grouping_calls(h, &[]);
13730 }
13731 }
13732 }
13733 if grouping_sets.len() > 1 {
13734 // The primary set's own dropped keys nullify in the
13735 // HEAD's projection too (GROUPING SETS's first set may
13736 // omit keys other sets use).
13737 let primary = grouping_sets[0].clone();
13738 let head_dropped: Vec<Expr> = grouping_universe
13739 .iter()
13740 .filter(|u| !primary.iter().any(|k| k == *u))
13741 .cloned()
13742 .collect();
13743 for set in grouping_sets.iter().skip(1) {
13744 let mut peer = stmt.clone();
13745 peer.unions = Vec::new();
13746 let dropped: Vec<&Expr> = grouping_universe
13747 .iter()
13748 .filter(|u| !set.iter().any(|k| k == *u))
13749 .collect();
13750 // Empty set = grand-total group: `Some(vec![])` forces
13751 // the aggregate path (one collapsed row) instead of a
13752 // per-row passthrough. See the primary-set note above.
13753 peer.group_by = Some(set.clone());
13754 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13755 for item in &mut peer.items {
13756 if let SelectItem::Expr { expr, alias } = item {
13757 if dropped.iter().any(|d| *d == expr) {
13758 // v7.39 — keep the dropped key's name on the
13759 // NULL literal so the UNION output column
13760 // (and any top-level ORDER BY on it) still
13761 // resolves.
13762 if alias.is_none()
13763 && let Expr::Column(c) = &expr
13764 {
13765 *alias = Some(c.name.clone());
13766 }
13767 *expr = Expr::Literal(Literal::Null);
13768 } else {
13769 Self::substitute_grouping_calls(expr, &dropped_owned);
13770 }
13771 }
13772 }
13773 if let Some(h) = &mut peer.having {
13774 Self::substitute_grouping_calls(h, &dropped_owned);
13775 }
13776 stmt.unions.push((UnionKind::All, peer));
13777 }
13778 for item in &mut stmt.items {
13779 if let SelectItem::Expr { expr, alias } = item {
13780 if head_dropped.iter().any(|d| d == expr) {
13781 if alias.is_none()
13782 && let Expr::Column(c) = &expr
13783 {
13784 *alias = Some(c.name.clone());
13785 }
13786 *expr = Expr::Literal(Literal::Null);
13787 } else {
13788 Self::substitute_grouping_calls(expr, &head_dropped);
13789 }
13790 }
13791 }
13792 if let Some(h) = &mut stmt.having {
13793 Self::substitute_grouping_calls(h, &head_dropped);
13794 }
13795 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13796 // (while `grouping_universe` / the per-branch sets are in scope). For
13797 // each grouping() call in it, inject a per-branch hidden column
13798 // `__grp_ord_K` carrying that branch's mask into the head + every
13799 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13800 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13801 // from the final output. A standalone grouping-set query has ORDER BY
13802 // (not an explicit set-op) next, so consuming it here is safe.
13803 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13804 // rollup carries the hierarchical order: sort by the grouping
13805 // keys with the rolled-up NULLs last, which is exactly the
13806 // interleaving both oracles emit. A client's own ORDER BY wins,
13807 // which is what MySQL does (MariaDB refuses to let one be
13808 // written at all).
13809 // The synthesised keys have to travel the SAME path a written
13810 // ORDER BY does: the block below is what turns a `grouping()`
13811 // call into the per-branch `__grp_ord_K` column the engine can
13812 // actually sort on. Bypassing it left a bare `grouping(text)`
13813 // for the evaluator to reject.
13814 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13815 self.parse_order_by_keys()?
13816 } else if mysql_rollup {
13817 Self::mysql_rollup_order(&grouping_universe)
13818 } else {
13819 Vec::new()
13820 };
13821 if !synthesised_or_parsed.is_empty() {
13822 let mut order_keys = synthesised_or_parsed;
13823 let mut grp_exprs: Vec<Expr> = Vec::new();
13824 for ob in &order_keys {
13825 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13826 }
13827 for (k, gexpr) in grp_exprs.iter().enumerate() {
13828 let colname = alloc::format!("__grp_ord_{k}");
13829 // Head branch (primary set) uses `head_dropped`.
13830 let mut he = gexpr.clone();
13831 Self::substitute_grouping_calls(&mut he, &head_dropped);
13832 stmt.items.push(SelectItem::Expr {
13833 expr: he,
13834 alias: Some(colname.clone()),
13835 });
13836 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13837 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13838 let set = &grouping_sets[i + 1];
13839 let dropped: Vec<Expr> = grouping_universe
13840 .iter()
13841 .filter(|u| !set.iter().any(|k| k == *u))
13842 .cloned()
13843 .collect();
13844 let mut pe = gexpr.clone();
13845 Self::substitute_grouping_calls(&mut pe, &dropped);
13846 peer.items.push(SelectItem::Expr {
13847 expr: pe,
13848 alias: Some(colname.clone()),
13849 });
13850 }
13851 }
13852 for ob in &mut order_keys {
13853 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13854 }
13855 stmt.order_by = order_keys;
13856 }
13857 }
13858 Ok(stmt)
13859 }
13860
13861 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13862 /// as ORDER BY keys.
13863 ///
13864 /// Per key: the rollup marker, then the key. Sorting on the key alone
13865 /// is not enough, and a table with a NULL in it says why — MariaDB puts
13866 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13867 /// the ROLLUP-introduced NULL last, and both print as NULL.
13868 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13869 /// real group including the data-NULL one, 1 only for the row the
13870 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13871 /// rolls up to NULL|2, a|1, b|3, NULL|6.
13872 ///
13873 /// `#[inline(never)]`: its locals must not join the statement parser's
13874 /// frame, which round 430 measured sitting against the nesting guard.
13875 #[inline(never)]
13876 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13877 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13878 for e in keys {
13879 out.push(OrderBy {
13880 expr: Expr::FunctionCall {
13881 name: "grouping".into(),
13882 args: alloc::vec![e.clone()],
13883 },
13884 desc: false,
13885 nulls_first: None,
13886 collation: None,
13887 });
13888 out.push(OrderBy {
13889 expr: e.clone(),
13890 desc: false,
13891 // MySQL orders NULL first on an ascending key.
13892 nulls_first: Some(true),
13893 collation: None,
13894 });
13895 }
13896 out
13897 }
13898
13899 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13900 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13901 #[inline(never)]
13902 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13903 use crate::ast::MaintainKind;
13904 self.skip_paren_option_list();
13905 let kind = match self.peek() {
13906 // `TABLE` and `INDEX` lex as keywords, not identifiers.
13907 Token::Table | Token::Index => {
13908 self.advance();
13909 MaintainKind::ReindexRelation
13910 }
13911 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13912 "index" | "table" => {
13913 self.advance();
13914 MaintainKind::ReindexRelation
13915 }
13916 "schema" => {
13917 self.advance();
13918 MaintainKind::ReindexSchema
13919 }
13920 "system" | "database" => {
13921 self.advance();
13922 MaintainKind::Whole
13923 }
13924 // PG requires the object type; anything else is the
13925 // caller's problem, not something to swallow.
13926 _ => MaintainKind::ReindexRelation,
13927 },
13928 _ => MaintainKind::Whole,
13929 };
13930 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13931 // allows the plain form, so the modifier is recorded rather than
13932 // skipped. It still has no effect on how the reindex runs.
13933 let mut concurrently = false;
13934 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13935 self.advance();
13936 concurrently = true;
13937 }
13938 let target = self.take_optional_maintain_name();
13939 self.consume_until_statement_boundary();
13940 Ok(Statement::Maintain {
13941 kind,
13942 concurrently,
13943 target,
13944 })
13945 }
13946
13947 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13948 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13949 #[inline(never)]
13950 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13951 use crate::ast::MaintainKind;
13952 self.skip_paren_option_list();
13953 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13954 self.advance();
13955 }
13956 let target = self.take_optional_maintain_name();
13957 self.consume_until_statement_boundary();
13958 Ok(Statement::Maintain {
13959 kind: if target.is_some() {
13960 MaintainKind::ClusterRelation
13961 } else {
13962 MaintainKind::Whole
13963 },
13964 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13965 // transaction block quite happily (measured).
13966 concurrently: false,
13967 target,
13968 })
13969 }
13970
13971 /// The next token as a relation / schema name, when there is one.
13972 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13973 match self.peek() {
13974 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13975 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13976 _ => None,
13977 },
13978 _ => None,
13979 }
13980 }
13981
13982 /// A parenthesised option list, absorbed.
13983 fn skip_paren_option_list(&mut self) {
13984 if !matches!(self.peek(), Token::LParen) {
13985 return;
13986 }
13987 let mut depth = 0usize;
13988 loop {
13989 match self.advance() {
13990 Token::LParen => depth += 1,
13991 Token::RParen => {
13992 depth -= 1;
13993 if depth == 0 {
13994 return;
13995 }
13996 }
13997 Token::Eof => return,
13998 _ => {}
13999 }
14000 }
14001 }
14002
14003 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14004 /// column list.
14005 ///
14006 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14007 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14008 /// / ALL. The three that describe physical storage have no meaning
14009 /// here, so they parse and change nothing rather than making a
14010 /// dump that mentions them fail to load.
14011 ///
14012 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14013 /// parse chain the nesting sentinel is tuned against.
14014 #[inline(never)]
14015 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14016 self.advance(); // LIKE
14017 let source = self.expect_ident_like()?;
14018 let mut options = crate::ast::LikeOptions::default();
14019 loop {
14020 let including = match self.peek() {
14021 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14022 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14023 _ => break,
14024 };
14025 self.advance();
14026 // `ALL` lexes as its own keyword, not an identifier.
14027 let opt = if matches!(self.peek(), Token::All) {
14028 self.advance();
14029 alloc::string::String::from("all")
14030 } else {
14031 self.expect_ident_like()?
14032 };
14033 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14034 o.defaults = on;
14035 o.constraints = on;
14036 o.identity = on;
14037 o.generated = on;
14038 o.indexes = on;
14039 o.comments = on;
14040 };
14041 match opt.to_ascii_lowercase().as_str() {
14042 "all" => set(&mut options, including),
14043 "defaults" => options.defaults = including,
14044 "constraints" => options.constraints = including,
14045 "identity" => options.identity = including,
14046 "generated" => options.generated = including,
14047 "indexes" => options.indexes = including,
14048 "comments" => options.comments = including,
14049 // No storage model to copy into.
14050 "storage" | "statistics" | "compression" => {}
14051 other => {
14052 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14053 }
14054 }
14055 }
14056 Ok(crate::ast::LikeSpec {
14057 source,
14058 at,
14059 options,
14060 })
14061 }
14062
14063 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14064 // Caller already consumed CREATE; we're sitting on TABLE.
14065 debug_assert!(matches!(self.peek(), Token::Table));
14066 self.advance();
14067 let if_not_exists = self.consume_if_not_exists();
14068 let name = self.expect_ident_like()?;
14069 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14070 // child shape has no column list; the child inherits its
14071 // columns from the parent at engine-DDL time. Detect it
14072 // before the `(` requirement below.
14073 if matches!(self.peek(), Token::Partition)
14074 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14075 {
14076 self.advance(); // PARTITION
14077 self.advance(); // of
14078 let partition_of = self.parse_partition_of_tail()?;
14079 return Ok(Statement::CreateTable(CreateTableStatement {
14080 temporary: false,
14081 name,
14082 columns: Vec::new(),
14083 like_specs: Vec::new(),
14084 inherits: Vec::new(),
14085 if_not_exists,
14086 foreign_keys: Vec::new(),
14087 table_constraints: Vec::new(),
14088 partition_by: None,
14089 partition_of: Some(partition_of),
14090 }));
14091 }
14092 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14093 // the materialized-view materialisation path (run the SELECT, infer the
14094 // column types, create + populate the table) but marks the node so the
14095 // executor creates a plain table without a mat-view registry entry.
14096 if matches!(self.peek(), Token::As) {
14097 self.advance();
14098 let body_stmt = self.parse_select_stmt()?;
14099 let Statement::Select(body) = body_stmt else {
14100 return Err(self.err(format!(
14101 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14102 )));
14103 };
14104 let with_data = self.parse_optional_with_data(true)?;
14105 return Ok(Statement::CreateMaterializedView(
14106 crate::ast::CreateMaterializedViewStatement {
14107 temporary: false,
14108 name,
14109 if_not_exists,
14110 columns: Vec::new(),
14111 body,
14112 with_data,
14113 as_plain_table: true,
14114 },
14115 ));
14116 }
14117 if !matches!(self.peek(), Token::LParen) {
14118 return Err(self.err(format!(
14119 "expected '(' after table name, got {:?}",
14120 self.peek()
14121 )));
14122 }
14123 self.advance();
14124 let mut columns = Vec::new();
14125 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14126 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14127 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14128 loop {
14129 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14130 // column list. It is how a child that adds nothing of its own is
14131 // written, and this loop demanded at least one entry: `syntax
14132 // error at or near ")"`. The child takes the parent's columns,
14133 // which the INHERITS clause already arranges.
14134 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14135 self.advance();
14136 break;
14137 }
14138 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14139 // clauses from column definitions. Constraints start
14140 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14141 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14142 // a column.
14143 if self.peek_table_level_pk_start() {
14144 table_constraints.push(self.parse_table_level_primary_key()?);
14145 } else if matches!(self.peek(), Token::Like) {
14146 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14147 // <opt> ]*`. The source table's shape lives in the catalog,
14148 // so this records the clause and the engine expands it.
14149 like_specs.push(self.parse_create_table_like(columns.len())?);
14150 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14151 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14152 table_constraints.push(self.parse_table_level_exclude()?);
14153 } else if self.peek_table_level_unique_start() {
14154 table_constraints.push(self.parse_table_level_unique()?);
14155 } else if self.peek_table_level_check_start() {
14156 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14157 table_constraints.push(self.parse_table_level_check()?);
14158 } else if self.peek_mysql_inline_key_start() {
14159 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14160 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14161 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14162 // inside the column list. Skip name + paren list;
14163 // for UNIQUE KEY, register as a UC.
14164 if let Some(uc) = self.parse_mysql_inline_key()? {
14165 table_constraints.push(uc);
14166 }
14167 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14168 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14169 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14170 // CHECK is named, and the named-CONSTRAINT arm used
14171 // to accept FOREIGN KEY only. The name is accepted
14172 // and discarded — same handling as every other SPG
14173 // constraint name.
14174 self.advance(); // CONSTRAINT
14175 // v7.39 (read01 round 48) — the name is kept now: the schema
14176 // stores it, so DROP / RENAME CONSTRAINT can find it.
14177 let con_name = self.expect_ident_like()?;
14178 let mut tc = match kind {
14179 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14180 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14181 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14182 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14183 };
14184 match &mut tc {
14185 crate::ast::TableConstraint::Check { name, .. }
14186 | crate::ast::TableConstraint::Unique { name, .. }
14187 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14188 | crate::ast::TableConstraint::Exclude { name, .. } => {
14189 *name = Some(con_name);
14190 }
14191 _ => {}
14192 }
14193 table_constraints.push(tc);
14194 } else if self.peek_constraint_or_fk_start() {
14195 foreign_keys.push(self.parse_table_level_fk()?);
14196 } else {
14197 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14198 // v7.13.0 — fold inline UNIQUE / CHECK column
14199 // constraints into table-level entries so the
14200 // engine path stays uniform.
14201 if col.is_unique {
14202 table_constraints.push(crate::ast::TableConstraint::Unique {
14203 name: None,
14204 columns: alloc::vec![col.name.clone()],
14205 nulls_not_distinct: col.unique_nulls_not_distinct,
14206 deferrable: col.constraint_deferrable,
14207 initially_deferred: col.constraint_initially_deferred,
14208 });
14209 }
14210 if let Some(check_expr) = col.check.clone() {
14211 table_constraints.push(crate::ast::TableConstraint::Check {
14212 name: None,
14213 expr: check_expr,
14214 not_valid: false,
14215 });
14216 }
14217 columns.push(col);
14218 if let Some(fk) = col_level_fk {
14219 foreign_keys.push(fk);
14220 }
14221 }
14222 match self.peek() {
14223 Token::Comma => {
14224 self.advance();
14225 }
14226 Token::RParen => {
14227 self.advance();
14228 break;
14229 }
14230 other => {
14231 return Err(
14232 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14233 );
14234 }
14235 }
14236 }
14237 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14238 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14239 // nothing is written between the parentheses.
14240 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14241 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14242 // empty parentheses were a parse error in their own right — quite apart
14243 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14244 // SPG does not have (filed separately).
14245 let _ = &like_specs;
14246 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14247 // It sits between the column list and the MySQL table options,
14248 // and it was a syntax error until this round.
14249 let mut inherits: Vec<String> = Vec::new();
14250 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14251 if k.eq_ignore_ascii_case("inherits"))
14252 {
14253 self.advance();
14254 if !matches!(self.peek(), Token::LParen) {
14255 return Err(self.err(alloc::format!(
14256 "expected ( after INHERITS, got {:?}",
14257 self.peek()
14258 )));
14259 }
14260 self.advance();
14261 loop {
14262 inherits.push(self.expect_ident_like()?);
14263 if matches!(self.peek(), Token::Comma) {
14264 self.advance();
14265 continue;
14266 }
14267 break;
14268 }
14269 if !matches!(self.peek(), Token::RParen) {
14270 return Err(self.err(alloc::format!(
14271 "expected ) closing INHERITS, got {:?}",
14272 self.peek()
14273 )));
14274 }
14275 self.advance();
14276 }
14277 // v7.14.0 — consume MySQL/MariaDB table options after the
14278 // closing `)`. mysqldump emits things like
14279 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14280 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14281 // SPG accepts all forms as no-ops (each option is
14282 // `<ident> [=] <ident-or-string>` separated by whitespace).
14283 self.consume_mysql_table_options();
14284 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14285 // SPG has no per-table reloptions, so accept and ignore them so a
14286 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14287 self.consume_with_reloptions();
14288 // v7.37.6-B — declarative-partition-parent suffix
14289 // (`PARTITION BY RANGE (key_col)`) sits after the column
14290 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14291 // and locks the key column at one ident; the engine then
14292 // verifies the column type is TIMESTAMPTZ.
14293 let partition_by = if matches!(self.peek(), Token::Partition) {
14294 self.advance(); // PARTITION
14295 if !self.peek_is_by() {
14296 return Err(self.err(format!(
14297 "expected BY after PARTITION, got {:?}",
14298 self.peek()
14299 )));
14300 }
14301 self.advance();
14302 Some(self.parse_partition_by_tail()?)
14303 } else {
14304 None
14305 };
14306 Ok(Statement::CreateTable(CreateTableStatement {
14307 temporary: false,
14308 name,
14309 columns,
14310 like_specs,
14311 inherits,
14312 if_not_exists,
14313 foreign_keys,
14314 table_constraints,
14315 partition_by,
14316 partition_of: None,
14317 }))
14318 }
14319
14320 /// v7.37.6-B — case-insensitive ident match helper for the
14321 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14322 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14323 /// didn't burn a global keyword slot for each (see the
14324 /// `Token::Partition` doc-comment in `lexer.rs`).
14325 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14326 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14327 }
14328
14329 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14330 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14331 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14332 use crate::ast::{PartitionBySpec, PartitionKindAst};
14333 let kind = match self.peek() {
14334 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14335 self.advance();
14336 PartitionKindAst::Range
14337 }
14338 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14339 self.advance();
14340 PartitionKindAst::List
14341 }
14342 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14343 self.advance();
14344 PartitionKindAst::Hash
14345 }
14346 other => {
14347 return Err(self.err(format!(
14348 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14349 )));
14350 }
14351 };
14352 if !matches!(self.peek(), Token::LParen) {
14353 return Err(self.err(format!(
14354 "expected '(' after PARTITION BY <strategy>, got {:?}",
14355 self.peek()
14356 )));
14357 }
14358 self.advance();
14359 let mut key_columns = Vec::new();
14360 loop {
14361 key_columns.push(self.expect_ident_like()?);
14362 match self.peek() {
14363 Token::Comma => {
14364 self.advance();
14365 }
14366 Token::RParen => {
14367 self.advance();
14368 break;
14369 }
14370 other => {
14371 return Err(self.err(format!(
14372 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14373 )));
14374 }
14375 }
14376 }
14377 if key_columns.is_empty() {
14378 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14379 }
14380 Ok(PartitionBySpec { kind, key_columns })
14381 }
14382
14383 /// v7.37.6-B — after `PARTITION OF`, expect
14384 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14385 /// or
14386 /// <parent> DEFAULT
14387 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14388 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14389 let parent_name = self.expect_ident_like()?;
14390 // v7.37.6-B rejects an explicit column list — the child
14391 // inherits from the parent. mailrs round-7 taught us that
14392 // CREATE TABLE-side schema reconciliation hides drift, so
14393 // we surface this as a parse error rather than silently
14394 // ignoring user columns.
14395 if matches!(self.peek(), Token::LParen) {
14396 return Err(self.err(
14397 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14398 at v7.37.6-B; the child inherits its columns from the parent"
14399 .to_string(),
14400 ));
14401 }
14402 let bounds = match self.peek() {
14403 Token::Default => {
14404 self.advance();
14405 PartitionOfBoundsAst::Default
14406 }
14407 Token::For => {
14408 self.advance();
14409 if !matches!(self.peek(), Token::Values) {
14410 return Err(
14411 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14412 );
14413 }
14414 self.advance();
14415 // WITH is not a reserved Token in the lexer — it lexes
14416 // as Token::Ident("with"). Disambiguate manually.
14417 let want_with = matches!(
14418 self.peek(),
14419 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14420 );
14421 if want_with {
14422 self.advance();
14423 if !matches!(self.peek(), Token::LParen) {
14424 return Err(self.err(format!(
14425 "expected '(' after FOR VALUES WITH, got {:?}",
14426 self.peek()
14427 )));
14428 }
14429 self.advance();
14430 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14431 loop {
14432 let key = self.expect_ident_like()?;
14433 let n = match self.peek().clone() {
14434 Token::Integer(v) if u32::try_from(v).is_ok() => {
14435 self.advance();
14436 v as u32
14437 }
14438 other => {
14439 return Err(self.err(format!(
14440 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14441 )));
14442 }
14443 };
14444 match key.to_ascii_uppercase().as_str() {
14445 "MODULUS" => modulus = Some(n),
14446 "REMAINDER" => remainder = Some(n),
14447 other => {
14448 return Err(self.err(format!(
14449 "FOR VALUES WITH: unknown key {other:?}; \
14450 expected MODULUS or REMAINDER"
14451 )));
14452 }
14453 }
14454 match self.peek() {
14455 Token::Comma => {
14456 self.advance();
14457 }
14458 Token::RParen => {
14459 self.advance();
14460 break;
14461 }
14462 other => {
14463 return Err(self.err(format!(
14464 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14465 )));
14466 }
14467 }
14468 }
14469 let modulus = modulus
14470 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14471 let remainder = remainder.ok_or_else(|| {
14472 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14473 })?;
14474 if modulus == 0 {
14475 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14476 }
14477 if remainder >= modulus {
14478 return Err(self.err(format!(
14479 "FOR VALUES WITH: REMAINDER ({remainder}) \
14480 must be < MODULUS ({modulus})"
14481 )));
14482 }
14483 PartitionOfBoundsAst::Hash { modulus, remainder }
14484 } else {
14485 match self.peek() {
14486 Token::From => {
14487 self.advance();
14488 let lower = Box::new(self.parse_partition_bound_expr()?);
14489 if !matches!(self.peek(), Token::To) {
14490 return Err(self.err(format!(
14491 "expected TO after FROM (...), got {:?}",
14492 self.peek()
14493 )));
14494 }
14495 self.advance();
14496 let upper = Box::new(self.parse_partition_bound_expr()?);
14497 PartitionOfBoundsAst::Range { lower, upper }
14498 }
14499 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14500 Token::In => {
14501 self.advance();
14502 if !matches!(self.peek(), Token::LParen) {
14503 return Err(self.err(format!(
14504 "expected '(' after FOR VALUES IN, got {:?}",
14505 self.peek()
14506 )));
14507 }
14508 self.advance();
14509 let mut values = Vec::new();
14510 loop {
14511 values.push(self.parse_expr(0)?);
14512 match self.peek() {
14513 Token::Comma => {
14514 self.advance();
14515 }
14516 Token::RParen => {
14517 self.advance();
14518 break;
14519 }
14520 other => {
14521 return Err(self.err(format!(
14522 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14523 )));
14524 }
14525 }
14526 }
14527 if values.is_empty() {
14528 return Err(self.err(
14529 "FOR VALUES IN requires at least one literal".to_string(),
14530 ));
14531 }
14532 PartitionOfBoundsAst::List { values }
14533 }
14534 other => {
14535 return Err(self.err(format!(
14536 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14537 )));
14538 }
14539 }
14540 }
14541 }
14542 other => {
14543 return Err(self.err(format!(
14544 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14545 )));
14546 }
14547 };
14548 Ok(PartitionOfSpec {
14549 parent_name,
14550 bounds,
14551 })
14552 }
14553
14554 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14555 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14556 /// markers (no-arg builtins) so the engine resolves them
14557 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14558 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14559 if !matches!(self.peek(), Token::LParen) {
14560 return Err(self.err(format!(
14561 "expected '(' before partition bound, got {:?}",
14562 self.peek()
14563 )));
14564 }
14565 self.advance();
14566 let expr = match self.peek() {
14567 Token::Ident(s) | Token::QuotedIdent(s)
14568 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14569 {
14570 let name = s.to_ascii_uppercase();
14571 self.advance();
14572 crate::ast::Expr::FunctionCall {
14573 name,
14574 args: Vec::new(),
14575 }
14576 }
14577 _ => self.parse_expr(0)?,
14578 };
14579 if !matches!(self.peek(), Token::RParen) {
14580 return Err(self.err(format!(
14581 "expected ')' after partition bound, got {:?}",
14582 self.peek()
14583 )));
14584 }
14585 self.advance();
14586 Ok(expr)
14587 }
14588
14589 /// v7.14.0 — true when the next tokens look like an inline
14590 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14591 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14592 /// — each followed by an optional name + `(...)`. Critical:
14593 /// a column NAMED `key` / `index` (PG accepts as ident) must
14594 /// NOT be mistaken for the KEY constraint shape. We disambig
14595 /// by requiring the keyword to be followed by either `(` or
14596 /// `<ident> (`.
14597 fn peek_mysql_inline_key_start(&self) -> bool {
14598 let cur = self.peek();
14599 // Shapes:
14600 // KEY (cols)
14601 // KEY name (cols)
14602 // INDEX (cols)
14603 // INDEX name (cols)
14604 // UNIQUE KEY [name] (cols)
14605 // UNIQUE INDEX [name] (cols)
14606 // FULLTEXT [KEY|INDEX] [name] (cols)
14607 // SPATIAL [KEY|INDEX] [name] (cols)
14608 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14609 // tokens at skip = the position AFTER the index-form
14610 // keywords (KEY/INDEX) have been consumed.
14611 match self.tokens.get(skip) {
14612 Some(Token::LParen) => true,
14613 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14614 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14615 }
14616 _ => false,
14617 }
14618 };
14619 // `INDEX` lexes as Token::Index (reserved), not as
14620 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14621 // start; the peek helper below handles either.
14622 let is_key_or_index_tok = |t: &Token| -> bool {
14623 matches!(t, Token::Index)
14624 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14625 };
14626 match cur {
14627 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14628 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14629 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14630 }
14631 Token::Ident(s)
14632 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14633 {
14634 let nxt = self.tokens.get(self.pos + 1);
14635 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14636 self.pos + 2
14637 } else {
14638 self.pos + 1
14639 };
14640 after_keyword_followed_by_paren_or_ident_paren(after_after)
14641 }
14642 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14643 let nxt = self.tokens.get(self.pos + 1);
14644 if !nxt.is_some_and(is_key_or_index_tok) {
14645 return false;
14646 }
14647 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14648 }
14649 _ => false,
14650 }
14651 }
14652
14653 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14654 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14655 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14656 /// returns Some(TableConstraint::Index) so the engine builds
14657 /// a real BTree index on the leading column (mysqldump
14658 /// `KEY idx_posts_author (author_id)` shape).
14659 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14660 /// (the storage layer has no matching AM).
14661 fn parse_mysql_inline_key(
14662 &mut self,
14663 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14664 // Detect UNIQUE prefix.
14665 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14666 {
14667 self.advance();
14668 true
14669 } else {
14670 false
14671 };
14672 // Consume FULLTEXT / SPATIAL prefix and record which one
14673 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14674 // dedicated TableConstraint variant so the engine can
14675 // build a tsvector-GIN; SPATIAL still has no matching
14676 // AM, so it falls back to accept-as-no-op.
14677 let mut is_fulltext = false;
14678 let mut is_spatial = false;
14679 if let Token::Ident(s) = self.peek().clone() {
14680 if s.eq_ignore_ascii_case("fulltext") {
14681 self.advance();
14682 is_fulltext = true;
14683 } else if s.eq_ignore_ascii_case("spatial") {
14684 self.advance();
14685 is_spatial = true;
14686 }
14687 }
14688 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14689 // (reserved); accept either token shape.
14690 match self.peek() {
14691 Token::Index => {
14692 self.advance();
14693 }
14694 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14695 self.advance();
14696 }
14697 other => {
14698 return Err(self.err(alloc::format!(
14699 "expected KEY/INDEX in inline index declaration, got {other:?}"
14700 )));
14701 }
14702 }
14703 // Optional index name (an ident before the `(`).
14704 // v7.15.0 — capture the name when present so the engine
14705 // builds the secondary index under the user's chosen
14706 // name (matches mysqldump's `KEY idx_x (col)` shape).
14707 let mut idx_name: Option<String> = None;
14708 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14709 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14710 {
14711 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14712 idx_name = Some(s);
14713 }
14714 }
14715 // Optional `USING BTREE` / `USING HASH` (MySQL).
14716 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14717 self.advance();
14718 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14719 self.advance();
14720 }
14721 }
14722 // Required column list `(col [, col]*)`.
14723 if !matches!(self.peek(), Token::LParen) {
14724 return Err(self.err(alloc::format!(
14725 "expected '(' in inline KEY/INDEX, got {:?}",
14726 self.peek()
14727 )));
14728 }
14729 self.advance();
14730 let mut cols: Vec<String> = Vec::new();
14731 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14732 self.advance();
14733 cols.push(s);
14734 // Skip optional `(length)` per-column prefix.
14735 if matches!(self.peek(), Token::LParen) {
14736 let mut depth = 1usize;
14737 self.advance();
14738 while depth > 0 {
14739 match self.peek() {
14740 Token::LParen => depth += 1,
14741 Token::RParen => depth -= 1,
14742 Token::Eof => break,
14743 _ => {}
14744 }
14745 self.advance();
14746 }
14747 }
14748 // Skip optional ASC / DESC.
14749 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14750 || matches!(self.peek(), Token::Asc | Token::Desc)
14751 {
14752 self.advance();
14753 }
14754 if matches!(self.peek(), Token::Comma) {
14755 self.advance();
14756 continue;
14757 }
14758 break;
14759 }
14760 if matches!(self.peek(), Token::RParen) {
14761 self.advance();
14762 }
14763 // Trailing options on the inline index — comment / etc.
14764 // Skip until comma or `)`.
14765 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14766 self.advance();
14767 }
14768 if cols.is_empty() {
14769 return Ok(None);
14770 }
14771 if is_unique {
14772 // Carry the captured idx_name on UNIQUE too so future
14773 // engine work can name the underlying BTree
14774 // accordingly; today the unique-constraint installer
14775 // synthesises the name itself, but Display round-trip
14776 // benefits from preserving it.
14777 Ok(Some(crate::ast::TableConstraint::Unique {
14778 name: idx_name,
14779 columns: cols,
14780 nulls_not_distinct: false,
14781 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14782 deferrable: false,
14783 initially_deferred: false,
14784 }))
14785 } else if is_fulltext {
14786 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14787 // routes through `TableConstraint::FulltextIndex`;
14788 // the engine builds a tsvector-GIN over each named
14789 // column so MATCH AGAINST gets a real inverted
14790 // index instead of a silently-dropped declaration.
14791 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14792 name: idx_name,
14793 columns: cols,
14794 }))
14795 } else if is_spatial {
14796 // SPG has no native SPATIAL AM. Accept-as-no-op
14797 // (declaration is parsed, but no index is built).
14798 Ok(None)
14799 } else {
14800 // v7.15.0 — plain KEY / INDEX builds a real BTree
14801 // secondary index.
14802 Ok(Some(crate::ast::TableConstraint::Index {
14803 name: idx_name,
14804 columns: cols,
14805 }))
14806 }
14807 }
14808
14809 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14810 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14811 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14812 /// (in any order, separated by whitespace).
14813 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14814 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14815 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14816 /// bare ident here, and only the parenthesised form is reloptions (so this
14817 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14818 fn consume_with_reloptions(&mut self) {
14819 let is_with = matches!(
14820 self.peek(),
14821 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14822 );
14823 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14824 return;
14825 }
14826 self.advance(); // WITH
14827 self.advance(); // (
14828 let mut depth = 1u32;
14829 while depth > 0 && !matches!(self.peek(), Token::Eof) {
14830 match self.peek() {
14831 Token::LParen => depth += 1,
14832 Token::RParen => depth -= 1,
14833 _ => {}
14834 }
14835 self.advance();
14836 }
14837 }
14838
14839 fn consume_mysql_table_options(&mut self) {
14840 loop {
14841 // Heuristic: a table option is an ident (or `DEFAULT`
14842 // reserved keyword) followed by `=` and an
14843 // ident / string / integer.
14844 let name_lc = match self.peek().clone() {
14845 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14846 Token::Default => alloc::string::String::from("default"),
14847 _ => break,
14848 };
14849 let known = matches!(
14850 name_lc.as_str(),
14851 "engine"
14852 | "default"
14853 | "charset"
14854 | "collate"
14855 | "auto_increment"
14856 | "row_format"
14857 | "comment"
14858 | "pack_keys"
14859 | "stats_persistent"
14860 | "stats_auto_recalc"
14861 | "stats_sample_pages"
14862 | "key_block_size"
14863 | "tablespace"
14864 | "min_rows"
14865 | "max_rows"
14866 | "checksum"
14867 | "delay_key_write"
14868 | "insert_method"
14869 | "data"
14870 | "index"
14871 | "encryption"
14872 | "compression"
14873 );
14874 if !known {
14875 break;
14876 }
14877 self.advance(); // option name
14878 // `DEFAULT` optional prefix is followed by `CHARSET` /
14879 // `COLLATE`; consume the next ident too.
14880 if name_lc == "default" {
14881 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14882 self.advance();
14883 }
14884 }
14885 if matches!(self.peek(), Token::Eq) {
14886 self.advance();
14887 }
14888 match self.peek() {
14889 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14890 self.advance();
14891 }
14892 _ => {}
14893 }
14894 }
14895 }
14896
14897 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14898 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14899 /// sure (otherwise a column literally named `primary` would
14900 /// be mistaken).
14901 fn peek_table_level_pk_start(&self) -> bool {
14902 let cur = self.peek();
14903 let nxt = self.tokens.get(self.pos + 1);
14904 let nxt2 = self.tokens.get(self.pos + 2);
14905 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14906 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14907 let is_lparen = matches!(nxt2, Some(Token::LParen));
14908 is_primary && is_key && is_lparen
14909 }
14910
14911 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14912 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14913 /// (mailrs round-5 G10).
14914 fn peek_table_level_unique_start(&self) -> bool {
14915 let cur = self.peek();
14916 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14917 if !is_unique {
14918 return false;
14919 }
14920 let n1 = self.tokens.get(self.pos + 1);
14921 // Plain `UNIQUE (…)`.
14922 if matches!(n1, Some(Token::LParen)) {
14923 return true;
14924 }
14925 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14926 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14927 if !is_nulls {
14928 return false;
14929 }
14930 let n2 = self.tokens.get(self.pos + 2);
14931 let n3 = self.tokens.get(self.pos + 3);
14932 let n4 = self.tokens.get(self.pos + 4);
14933 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14934 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14935 return true;
14936 }
14937 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14938 if matches!(n2, Some(Token::Not))
14939 && matches!(n3, Some(Token::Distinct))
14940 && matches!(n4, Some(Token::LParen))
14941 {
14942 return true;
14943 }
14944 false
14945 }
14946
14947 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14948 self.advance(); // PRIMARY
14949 self.advance(); // KEY
14950 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14951 // v7.39 (round 711) — the trailer's values are CARRIED now; round
14952 // 621 consumed and dropped them (the storing half of F08).
14953 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14954 Ok(crate::ast::TableConstraint::PrimaryKey {
14955 name: None,
14956 columns,
14957 deferrable,
14958 initially_deferred,
14959 })
14960 }
14961
14962 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14963 self.advance(); // UNIQUE
14964 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14965 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14966 // is `NULLS DISTINCT` per the SQL standard.
14967 let mut nulls_not_distinct = false;
14968 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14969 let n1 = self.tokens.get(self.pos + 1);
14970 let n2 = self.tokens.get(self.pos + 2);
14971 let is_not = matches!(n1, Some(Token::Not));
14972 let is_distinct = matches!(n2, Some(Token::Distinct));
14973 if is_not && is_distinct {
14974 self.advance(); // NULLS
14975 self.advance(); // NOT
14976 self.advance(); // DISTINCT
14977 nulls_not_distinct = true;
14978 } else if matches!(n1, Some(Token::Distinct)) {
14979 self.advance(); // NULLS
14980 self.advance(); // DISTINCT
14981 }
14982 }
14983 let columns = self.parse_paren_ident_list("UNIQUE")?;
14984 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14985 Ok(crate::ast::TableConstraint::Unique {
14986 name: None,
14987 columns,
14988 nulls_not_distinct,
14989 deferrable,
14990 initially_deferred,
14991 })
14992 }
14993
14994 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
14995 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
14996 /// expression.
14997 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
14998 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
14999 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15000 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15001 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15002 /// commit: `NOT` starts no other suffix here, but reading both
15003 /// tokens before advancing keeps the caller's error message intact
15004 /// if someone writes `NOT NULL` by mistake.
15005 fn parse_not_valid_suffix(&mut self) -> bool {
15006 if !matches!(self.peek(), Token::Not) {
15007 return false;
15008 }
15009 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15010 {
15011 return false;
15012 }
15013 self.advance();
15014 self.advance();
15015 true
15016 }
15017
15018 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15019 self.advance(); // EXCLUDE
15020 // Optional `USING <method>`.
15021 let mut method = None;
15022 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15023 self.advance();
15024 method = Some(match self.advance() {
15025 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15026 other => {
15027 return Err(self.err(alloc::format!(
15028 "expected index method after USING, got {other:?}"
15029 )));
15030 }
15031 });
15032 }
15033 if !matches!(self.peek(), Token::LParen) {
15034 return Err(self.err(alloc::format!(
15035 "expected '(' after EXCLUDE, got {:?}",
15036 self.peek()
15037 )));
15038 }
15039 self.advance();
15040 let mut elements: Vec<(String, String)> = Vec::new();
15041 loop {
15042 let col = match self.advance() {
15043 Token::Ident(s) | Token::QuotedIdent(s) => s,
15044 other => {
15045 return Err(self.err(alloc::format!(
15046 "expected column name in EXCLUDE, got {other:?}"
15047 )));
15048 }
15049 };
15050 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15051 return Err(self.err(alloc::format!(
15052 "expected WITH after EXCLUDE column, got {:?}",
15053 self.peek()
15054 )));
15055 }
15056 self.advance();
15057 let op = match self.advance() {
15058 Token::InetOverlap => String::from("&&"),
15059 Token::Intersects => String::from("?#"),
15060 Token::IsBelow => String::from("<^"),
15061 Token::IsAbove => String::from(">^"),
15062 Token::PatternLt => String::from("~<~"),
15063 Token::PatternLtEq => String::from("~<=~"),
15064 Token::PatternGt => String::from("~>~"),
15065 Token::PatternGtEq => String::from("~>=~"),
15066 Token::TsMatchOld => String::from("@@@"),
15067 Token::Eq => String::from("="),
15068 Token::JsonContains => String::from("@>"),
15069 Token::JsonContainedBy => String::from("<@"),
15070 Token::OverLeft => String::from("&<"),
15071 Token::OverRight => String::from("&>"),
15072 other => {
15073 return Err(self.err(alloc::format!(
15074 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15075 )));
15076 }
15077 };
15078 elements.push((col, op));
15079 if matches!(self.peek(), Token::Comma) {
15080 self.advance();
15081 continue;
15082 }
15083 break;
15084 }
15085 if !matches!(self.peek(), Token::RParen) {
15086 return Err(self.err(alloc::format!(
15087 "expected ')' to close EXCLUDE, got {:?}",
15088 self.peek()
15089 )));
15090 }
15091 self.advance();
15092 Ok(crate::ast::TableConstraint::Exclude {
15093 name: None,
15094 method,
15095 elements,
15096 })
15097 }
15098
15099 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15100 self.advance(); // CHECK
15101 if !matches!(self.peek(), Token::LParen) {
15102 return Err(self.err(alloc::format!(
15103 "expected '(' after CHECK, got {:?}",
15104 self.peek()
15105 )));
15106 }
15107 self.advance();
15108 let expr = self.parse_expr(0)?;
15109 if !matches!(self.peek(), Token::RParen) {
15110 return Err(self.err(alloc::format!(
15111 "expected ')' to close CHECK predicate, got {:?}",
15112 self.peek()
15113 )));
15114 }
15115 self.advance();
15116 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15117 // are no existing rows for PG to skip, so it rejects the suffix.
15118 Ok(crate::ast::TableConstraint::Check {
15119 name: None,
15120 expr,
15121 not_valid: false,
15122 })
15123 }
15124
15125 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15126 fn peek_table_level_check_start(&self) -> bool {
15127 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15128 }
15129
15130 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15131 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15132 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15133 /// own CONSTRAINT prefix).
15134 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15135 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15136 return None;
15137 }
15138 // tokens[pos+1] is the constraint name (any ident-like);
15139 // tokens[pos+2] is the kind keyword.
15140 match self.tokens.get(self.pos + 2) {
15141 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15142 Some(NamedTableConstraintKind::Check)
15143 }
15144 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15145 Some(NamedTableConstraintKind::Unique)
15146 }
15147 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15148 Some(NamedTableConstraintKind::PrimaryKey)
15149 }
15150 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15151 Some(NamedTableConstraintKind::Exclude)
15152 }
15153 _ => None,
15154 }
15155 }
15156
15157 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15158 if !matches!(self.peek(), Token::LParen) {
15159 return Err(self.err(alloc::format!(
15160 "expected '(' after {ctx}, got {:?}",
15161 self.peek()
15162 )));
15163 }
15164 self.advance();
15165 let mut out = Vec::new();
15166 loop {
15167 out.push(self.expect_ident_like()?);
15168 match self.peek() {
15169 Token::Comma => {
15170 self.advance();
15171 }
15172 Token::RParen => {
15173 self.advance();
15174 break;
15175 }
15176 other => {
15177 return Err(self.err(alloc::format!(
15178 "expected ',' or ')' in {ctx} list, got {other:?}"
15179 )));
15180 }
15181 }
15182 }
15183 if out.is_empty() {
15184 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15185 }
15186 Ok(out)
15187 }
15188
15189 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15190 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15191 /// table-level FK; a column def never starts with either keyword
15192 /// (column names are not in this reserved set).
15193 fn peek_constraint_or_fk_start(&self) -> bool {
15194 let is_constraint_kw = matches!(
15195 self.peek(),
15196 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15197 );
15198 let is_foreign_kw = matches!(
15199 self.peek(),
15200 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15201 );
15202 is_constraint_kw || is_foreign_kw
15203 }
15204
15205 /// v7.6.0 — parse a table-level FK clause:
15206 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15207 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15208 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15209 let mut name: Option<String> = None;
15210 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15211 self.advance();
15212 name = Some(self.expect_ident_like()?);
15213 }
15214 // `FOREIGN`
15215 match self.advance() {
15216 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15217 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15218 }
15219 // `KEY`
15220 match self.advance() {
15221 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15222 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15223 }
15224 // `(col, col, ...)`
15225 if !matches!(self.peek(), Token::LParen) {
15226 return Err(self.err(format!(
15227 "expected '(' after FOREIGN KEY, got {:?}",
15228 self.peek()
15229 )));
15230 }
15231 self.advance();
15232 let mut columns = Vec::new();
15233 loop {
15234 columns.push(self.expect_ident_like()?);
15235 match self.peek() {
15236 Token::Comma => {
15237 self.advance();
15238 }
15239 Token::RParen => {
15240 self.advance();
15241 break;
15242 }
15243 other => {
15244 return Err(self.err(format!(
15245 "expected ',' or ')' in FK column list, got {other:?}"
15246 )));
15247 }
15248 }
15249 }
15250 if columns.is_empty() {
15251 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15252 }
15253 let (
15254 parent_table,
15255 parent_columns,
15256 on_delete,
15257 on_update,
15258 match_type,
15259 deferrable,
15260 initially_deferred,
15261 ) = self.parse_references_tail(columns.len())?;
15262 Ok(ForeignKeyConstraint {
15263 name,
15264 columns,
15265 parent_table,
15266 parent_columns,
15267 on_delete,
15268 on_update,
15269 match_type,
15270 deferrable,
15271 initially_deferred,
15272 })
15273 }
15274
15275 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15276 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15277 /// the local column count, used to default the parent column
15278 /// list when omitted (SQL spec: parent's PK is implied).
15279 fn parse_references_tail(
15280 &mut self,
15281 expected_arity: usize,
15282 ) -> Result<
15283 (
15284 String,
15285 Vec<String>,
15286 FkAction,
15287 FkAction,
15288 crate::ast::MatchType,
15289 // v7.39 (round 288) — deferrable, initially_deferred.
15290 bool,
15291 bool,
15292 ),
15293 ParseError,
15294 > {
15295 match self.advance() {
15296 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15297 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15298 }
15299 let parent_table = self.expect_ident_like()?;
15300 let mut parent_columns: Vec<String> = Vec::new();
15301 if matches!(self.peek(), Token::LParen) {
15302 self.advance();
15303 loop {
15304 parent_columns.push(self.expect_ident_like()?);
15305 match self.peek() {
15306 Token::Comma => {
15307 self.advance();
15308 }
15309 Token::RParen => {
15310 self.advance();
15311 break;
15312 }
15313 other => {
15314 return Err(self.err(format!(
15315 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15316 )));
15317 }
15318 }
15319 }
15320 }
15321 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15322 return Err(self.err(format!(
15323 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15324 expected_arity,
15325 parent_columns.len()
15326 )));
15327 }
15328 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15329 // it between the referenced column list and the ON / DEFERRABLE
15330 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15331 // is skipped when any referencing column is NULL), so SIMPLE —
15332 // the default, and the only spelling pg_dump emits — is accepted
15333 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15334 // mixed-NULL rule, which is not wired yet; reject them honestly
15335 // rather than silently applying SIMPLE (PG itself errors on
15336 // MATCH PARTIAL as "not yet implemented").
15337 let mut match_type = crate::ast::MatchType::Simple;
15338 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15339 self.advance();
15340 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15341 // SIMPLE / PARTIAL arrive as bare identifiers.
15342 let kind = match self.advance() {
15343 Token::Full => "FULL".to_string(),
15344 Token::Ident(s) => s.to_uppercase(),
15345 other => {
15346 return Err(self.err(format!(
15347 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15348 )));
15349 }
15350 };
15351 match kind.as_str() {
15352 "SIMPLE" => {} // Default — match_type stays Simple.
15353 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15354 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15355 "FULL" => match_type = crate::ast::MatchType::Full,
15356 "PARTIAL" => {
15357 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15358 }
15359 _ => {
15360 return Err(self.err(format!(
15361 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15362 )));
15363 }
15364 }
15365 }
15366 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15367 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15368 // <action>` / `ON UPDATE <action>` in either order. PG /
15369 // pg_dump emits the timing clause AFTER the ON clauses
15370 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15371 // but the SQL spec allows either order. We loop over
15372 // every possible trailer and dispatch on the next token,
15373 // stopping when nothing matches. Phase 3.1 changes the
15374 // bare DEFERRABLE form from hard-error to accept-as-
15375 // immediate; SPG is single-writer with no deferred-
15376 // constraint window so the runtime semantics are always
15377 // immediate even when INITIALLY DEFERRED is requested.
15378 // PG's default referential action (no ON DELETE / ON UPDATE
15379 // clause) is NO ACTION, not RESTRICT — the two enforce
15380 // identically in SPG (single-writer, no deferred window; see the
15381 // shared match arm in constraints.rs) but information_schema.
15382 // referential_constraints must report NO ACTION to match PG.
15383 let mut on_delete = FkAction::NoAction;
15384 let mut on_update = FkAction::NoAction;
15385 let mut seen_on_delete = false;
15386 let mut seen_on_update = false;
15387 let mut deferrable = false;
15388 let mut initially_deferred = false;
15389 loop {
15390 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15391 let before = self.pos;
15392 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15393 if self.pos != before {
15394 deferrable = d;
15395 initially_deferred = idef;
15396 continue;
15397 }
15398 // ON DELETE / ON UPDATE.
15399 if !matches!(self.peek(), Token::On) {
15400 break;
15401 }
15402 self.advance();
15403 let which = self.advance();
15404 let action = self.parse_fk_action()?;
15405 match which {
15406 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15407 if seen_on_delete {
15408 return Err(self.err("ON DELETE specified twice".into()));
15409 }
15410 seen_on_delete = true;
15411 on_delete = action;
15412 }
15413 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15414 if seen_on_update {
15415 return Err(self.err("ON UPDATE specified twice".into()));
15416 }
15417 seen_on_update = true;
15418 on_update = action;
15419 }
15420 other => {
15421 return Err(
15422 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15423 );
15424 }
15425 }
15426 }
15427 Ok((
15428 parent_table,
15429 parent_columns,
15430 on_delete,
15431 on_update,
15432 match_type,
15433 deferrable,
15434 initially_deferred,
15435 ))
15436 }
15437
15438 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15439 /// NO ACTION`.
15440 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15441 match self.advance() {
15442 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15443 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15444 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15445 Token::Null => Ok(FkAction::SetNull),
15446 Token::Default => Ok(FkAction::SetDefault),
15447 other => Err(self.err(format!(
15448 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15449 ))),
15450 },
15451 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15452 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15453 other => Err(self.err(format!(
15454 "expected ACTION after NO in FK action, got {other:?}"
15455 ))),
15456 },
15457 other => Err(self.err(format!(
15458 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15459 ))),
15460 }
15461 }
15462
15463 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15464 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15465 fn consume_if_not_exists(&mut self) -> bool {
15466 // `IF` arrives as a bare Ident (we don't reserve it because it
15467 // also appears mid-expression in PG, though we don't support
15468 // those forms yet).
15469 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15470 if !looks_like_if {
15471 return false;
15472 }
15473 // Peek one ahead before committing: only consume IF when it's
15474 // actually `IF NOT EXISTS`.
15475 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15476 return false;
15477 }
15478 if !matches!(
15479 self.tokens.get(self.pos + 2),
15480 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15481 ) {
15482 return false;
15483 }
15484 self.advance(); // IF
15485 self.advance(); // NOT
15486 self.advance(); // EXISTS
15487 true
15488 }
15489
15490 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15491 /// Consumes IF EXISTS as a pair; returns false otherwise
15492 /// without consuming any tokens.
15493 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15494 /// ENABLE/DISABLE/FORCE/NO FORCE.
15495 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15496 for kw in ["row", "level", "security"] {
15497 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15498 {
15499 return Err(self.err(alloc::format!(
15500 "expected {} in ROW LEVEL SECURITY, got {:?}",
15501 kw.to_ascii_uppercase(),
15502 self.peek()
15503 )));
15504 }
15505 self.advance();
15506 }
15507 Ok(())
15508 }
15509
15510 fn consume_if_exists(&mut self) -> bool {
15511 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15512 if !looks_like_if {
15513 return false;
15514 }
15515 if !matches!(
15516 self.tokens.get(self.pos + 1),
15517 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15518 ) {
15519 return false;
15520 }
15521 self.advance(); // IF
15522 self.advance(); // EXISTS
15523 true
15524 }
15525
15526 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15527 /// qualifiers after an index column ref. ASC / DESC are
15528 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15529 /// We accept and discard them since single-column BTree
15530 /// stores rows in natural key order today.
15531 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15532 /// ORDER BY key. Returns None when absent.
15533 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15534 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15535 return Ok(None);
15536 }
15537 self.advance();
15538 match self.advance() {
15539 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15540 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15541 other => Err(self.err(alloc::format!(
15542 "expected FIRST or LAST after NULLS, got {other:?}"
15543 ))),
15544 }
15545 }
15546
15547 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15548 /// rather than discarded.
15549 ///
15550 /// SPG's index does not scan in a direction — column ordering is
15551 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15552 /// reproduction of the DDL, and dropping the clause meant
15553 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15554 /// dump lost it, and a schema diff saw drift on every run.
15555 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15556 let mut order = crate::ast::IndexColumnOrder::default();
15557 loop {
15558 match self.peek() {
15559 Token::Asc => {
15560 self.advance();
15561 }
15562 Token::Desc => {
15563 order.descending = true;
15564 self.advance();
15565 }
15566 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15567 let look = self.tokens.get(self.pos + 1);
15568 if matches!(
15569 look,
15570 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15571 || k.eq_ignore_ascii_case("last")
15572 ) {
15573 self.advance();
15574 order.nulls_first = Some(matches!(
15575 self.advance(),
15576 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15577 ));
15578 } else {
15579 break;
15580 }
15581 }
15582 _ => break,
15583 }
15584 }
15585 order
15586 }
15587
15588 fn parse_create_index_stmt_after_create(
15589 &mut self,
15590 is_unique: bool,
15591 ) -> Result<Statement, ParseError> {
15592 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15593 debug_assert!(matches!(self.peek(), Token::Index));
15594 self.advance();
15595 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15596 // SPG's CREATE INDEX is synchronous end-to-end today (real
15597 // CONCURRENTLY variant with restartable scans queues with
15598 // v7.39 indexes epic), so the modifier has no runtime effect
15599 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15600 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15601 // VIEW CONCURRENTLY.
15602 let mut concurrently = false;
15603 if matches!(
15604 self.peek(),
15605 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15606 ) {
15607 self.advance();
15608 concurrently = true;
15609 }
15610 let if_not_exists = self.consume_if_not_exists();
15611 // v7.39 (read01 round 93) — the index name is optional (PG since
15612 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15613 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15614 // was given; leave it empty and the engine derives a PG-style
15615 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15616 let name = if matches!(self.peek(), Token::On) {
15617 String::new()
15618 } else {
15619 self.expect_ident_like()?
15620 };
15621 if !matches!(self.peek(), Token::On) {
15622 return Err(self.err(format!(
15623 "expected ON after CREATE INDEX <name>, got {:?}",
15624 self.peek()
15625 )));
15626 }
15627 self.advance();
15628 let table = self.expect_ident_like()?;
15629 // Optional `USING <method>` — only recognised method in v2.0 is
15630 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15631 // ident `using` (we don't promote it to a reserved keyword
15632 // because it isn't reserved anywhere else in our SQL surface).
15633 let mut method_name: Option<String> = None;
15634 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15635 self.advance();
15636 let m = self.expect_ident_like()?;
15637 method_name = Some(m.to_ascii_lowercase());
15638 match m.to_ascii_lowercase().as_str() {
15639 "hnsw" => IndexMethod::Hnsw,
15640 "btree" => IndexMethod::BTree,
15641 "brin" => IndexMethod::Brin,
15642 // v7.12.3 — real GIN inverted index over `tsvector`.
15643 // v7.9.26b's `USING gin` → BTree silent fallback is
15644 // gone; the engine validates that the indexed column
15645 // is `tsvector` at CREATE INDEX time.
15646 "gin" => IndexMethod::Gin,
15647 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15648 // `USING spgist` / `USING hash` for their built-in
15649 // AMs that SPG doesn't have a matching
15650 // implementation for; degrade to BTree on the
15651 // leading column so the schema loads + the index
15652 // catalogue stays consistent. Operator pays the
15653 // planner cost only for the queries that would have
15654 // used the specialised AM.
15655 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15656 // v7.11.3 — pgvector ships both `ivfflat` and
15657 // `hnsw`. Customers shouldn't have to choose
15658 // their on-disk index method based on what SPG
15659 // implements; accept `ivfflat` as a synonym for
15660 // `hnsw` so PG schemas using either method drop
15661 // in. The vector distance op (`<->` / `<#>` /
15662 // `<=>`) at query time still picks the metric.
15663 "ivfflat" => IndexMethod::Hnsw,
15664 other => {
15665 return Err(self.err(alloc::format!(
15666 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15667 )));
15668 }
15669 }
15670 } else {
15671 IndexMethod::BTree
15672 };
15673 if !matches!(self.peek(), Token::LParen) {
15674 return Err(self.err(format!(
15675 "expected '(' before indexed column, got {:?}",
15676 self.peek()
15677 )));
15678 }
15679 self.advance();
15680 // v6.8.2 — accept either a bare column ident (legacy) or
15681 // an expression `fn(col, …)` for expression indexes.
15682 // Distinguish by peeking the token *after* the current
15683 // ident: `ident )` is the legacy column-only path;
15684 // anything else triggers the Pratt expression parser.
15685 // (`advance()` uses `mem::replace` to nil out the current
15686 // slot, so we can't save+rewind cleanly — peek-ahead via
15687 // direct index avoids the mutation.)
15688 let mut opclass: Option<String> = None;
15689 let mut key_collation: Option<String> = None;
15690 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15691 // Single column with `)` immediately after — fast path.
15692 // v7.9.29 — also: bare column followed by `,` (the
15693 // multi-column form `(a, b, c)`). Without this branch
15694 // the leading ident gets pulled into `parse_expr`
15695 // which then sets `expression = Some(Column(a))` and
15696 // breaks Display round-trip on the multi-column shape.
15697 Token::Ident(s) | Token::QuotedIdent(s)
15698 if matches!(
15699 self.tokens.get(self.pos + 1),
15700 Some(Token::RParen | Token::Comma)
15701 ) =>
15702 {
15703 self.advance();
15704 (s, None)
15705 }
15706 // v7.9.22 — single column followed by a pgvector
15707 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15708 // v7.15.0 — capture the opclass instead of discarding
15709 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15710 // → real trigram-shingle GIN over a TEXT column).
15711 // Vector/HNSW opclasses still take their distance
15712 // metric from the query operator (`<->` / `<#>` /
15713 // `<=>`), so for those callers the opclass stays
15714 // informational.
15715 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15716 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15717 // the schema and dispatch on the bare opclass, the same
15718 // treatment table/type names get.
15719 Token::Ident(s) | Token::QuotedIdent(s)
15720 if matches!(
15721 self.tokens.get(self.pos + 1),
15722 Some(Token::Ident(_) | Token::QuotedIdent(_))
15723 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15724 && matches!(
15725 self.tokens.get(self.pos + 3),
15726 Some(Token::Ident(op) | Token::QuotedIdent(op))
15727 if is_vector_opclass_name(op)
15728 ) =>
15729 {
15730 self.advance(); // column name
15731 self.advance(); // schema qualifier
15732 self.advance(); // dot
15733 let op_tok = self.advance();
15734 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15735 opclass = Some(op.to_ascii_lowercase());
15736 }
15737 (s, None)
15738 }
15739 // r1038 — an operator class is recognised by its POSITION, not
15740 // by a list of names. It used to be `is_vector_opclass_name`,
15741 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15742 // sentori's migration wrote — was a syntax error while
15743 // `USING gin (doc)` parsed. Anything sitting between a column
15744 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15745 // two bare identifiers in a row are not valid there otherwise.
15746 Token::Ident(s) | Token::QuotedIdent(s)
15747 if matches!(
15748 self.tokens.get(self.pos + 1),
15749 Some(Token::Ident(op) | Token::QuotedIdent(op))
15750 if is_vector_opclass_name(op) || Self::opclass_position_follows(
15751 self.tokens.get(self.pos + 2)
15752 )
15753 ) =>
15754 {
15755 self.advance(); // column name
15756 // Capture the opclass token, lower-cased for
15757 // case-insensitive engine dispatch.
15758 let op_tok = self.advance();
15759 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15760 opclass = Some(op.to_ascii_lowercase());
15761 }
15762 (s, None)
15763 }
15764 Token::Ident(_) | Token::QuotedIdent(_) => {
15765 // v7.39 (round 538) — an explicit COLLATE on the key,
15766 // read by LOOKAHEAD because `parse_expr` absorbs the
15767 // clause as a no-op (SPG orders text by bytes, which is
15768 // the C collation, so it changes nothing to honour). PG
15769 // still PRINTS it: an explicitly written `"C"` and the
15770 // collation a column inherits are different collation
15771 // OBJECTS even where they sort identically, which is why
15772 // `(a COLLATE "C")` shows on a C-collation database too.
15773 if matches!(
15774 self.tokens.get(self.pos + 1),
15775 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15776 ) {
15777 key_collation = match self.tokens.get(self.pos + 2) {
15778 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15779 Some(n.clone())
15780 }
15781 _ => None,
15782 };
15783 }
15784 let key_expr = self.parse_expr(0)?;
15785 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15786 self.err("expression index key must reference at least one column".into())
15787 })?;
15788 (primary, Some(key_expr))
15789 }
15790 // v7.37.43-T4 — parenthesised expression index key
15791 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15792 // PG's CREATE INDEX requires the expression to be in
15793 // its own parens to disambiguate function calls from
15794 // column lists, so this `LParen` is the inner open-paren
15795 // of an expression key. parse_expr handles the recursive
15796 // descent and consumes the matching `RParen`.
15797 Token::LParen => {
15798 let key_expr = self.parse_expr(0)?;
15799 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15800 self.err("expression index key must reference at least one column".into())
15801 })?;
15802 (primary, Some(key_expr))
15803 }
15804 other => {
15805 return Err(self.err(format!(
15806 "expected column ident or expression, got {other:?}"
15807 )));
15808 }
15809 };
15810 // v7.9.14 — accept extra comma-separated columns inside
15811 // the index key parens (`CREATE INDEX … (a, b, c)`).
15812 // mailrs F2. Each extra column may carry an optional
15813 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15814 // — parsed and discarded; SPG doesn't honour direction
15815 // on a BTree index today (column ordering is intrinsic
15816 // to the storage). v7.10 will widen to genuine composite
15817 // index keys.
15818 let mut extra_columns: Vec<String> = Vec::new();
15819 // The leading column may also have ASC/DESC after it — and that
15820 // one is the column SPG indexes, so its clause is kept.
15821 let key_order = self.consume_optional_index_column_qualifiers();
15822 while matches!(self.peek(), Token::Comma) {
15823 self.advance();
15824 let extra = self.expect_ident_like()?;
15825 let _ = self.consume_optional_index_column_qualifiers();
15826 extra_columns.push(extra);
15827 }
15828 if !matches!(self.peek(), Token::RParen) {
15829 return Err(self.err(format!(
15830 "expected ')' after indexed column / expression, got {:?}",
15831 self.peek()
15832 )));
15833 }
15834 self.advance();
15835 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15836 // index-only-scan annotation. Bare ident (not a reserved
15837 // keyword) so we test by case-insensitive string match.
15838 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15839 {
15840 self.advance();
15841 if !matches!(self.peek(), Token::LParen) {
15842 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15843 }
15844 self.advance();
15845 let mut cols = Vec::new();
15846 loop {
15847 cols.push(self.expect_ident_like()?);
15848 match self.peek() {
15849 Token::Comma => {
15850 self.advance();
15851 }
15852 Token::RParen => {
15853 self.advance();
15854 break;
15855 }
15856 other => {
15857 return Err(self.err(format!(
15858 "expected ',' or ')' in INCLUDE list, got {other:?}"
15859 )));
15860 }
15861 }
15862 }
15863 cols
15864 } else {
15865 Vec::new()
15866 };
15867 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15868 // storage parameters. pgvector emits `WITH (lists = N)` for
15869 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15870 // SPG's HNSW picks its own parameters today (tunable via
15871 // env vars), so the WITH clause is informational and dropped.
15872 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15873 self.advance();
15874 if !matches!(self.peek(), Token::LParen) {
15875 return Err(self.err(format!(
15876 "expected '(' after WITH in CREATE INDEX, got {:?}",
15877 self.peek()
15878 )));
15879 }
15880 self.advance();
15881 loop {
15882 if matches!(self.peek(), Token::RParen) {
15883 self.advance();
15884 break;
15885 }
15886 // Drain `key = value` or bare `key` tokens.
15887 let _ = self.advance(); // key
15888 if matches!(self.peek(), Token::Eq) {
15889 self.advance();
15890 let _ = self.advance(); // value (int / string / ident)
15891 }
15892 match self.peek() {
15893 Token::Comma => {
15894 self.advance();
15895 }
15896 Token::RParen => {
15897 self.advance();
15898 break;
15899 }
15900 other => {
15901 return Err(self.err(format!(
15902 "expected ',' or ')' in WITH (…) clause, got {other:?}"
15903 )));
15904 }
15905 }
15906 }
15907 }
15908 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15909 // which sits between the key list and the WHERE clause.
15910 let mut nulls_not_distinct = false;
15911 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15912 let n1 = self.tokens.get(self.pos + 1);
15913 let n2 = self.tokens.get(self.pos + 2);
15914 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15915 self.advance(); // NULLS
15916 self.advance(); // NOT
15917 self.advance(); // DISTINCT
15918 nulls_not_distinct = true;
15919 } else if matches!(n1, Some(Token::Distinct)) {
15920 self.advance(); // NULLS
15921 self.advance(); // DISTINCT
15922 }
15923 }
15924 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15925 let partial_predicate = if matches!(self.peek(), Token::Where) {
15926 self.advance();
15927 Some(self.parse_expr(0)?)
15928 } else {
15929 None
15930 };
15931 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15932 // sense: uniqueness over an ANN structure has no clean
15933 // semantics. Reject early. (BRIN UNIQUE is similarly
15934 // meaningless — block both.)
15935 if is_unique && !matches!(method, IndexMethod::BTree) {
15936 return Err(self.err(alloc::format!(
15937 "UNIQUE is only supported on BTree indexes, got USING {:?}",
15938 method
15939 )));
15940 }
15941 Ok(Statement::CreateIndex(CreateIndexStatement {
15942 concurrently,
15943 name,
15944 key_order,
15945 key_collation,
15946 table,
15947 column,
15948 nulls_not_distinct,
15949 method,
15950 if_not_exists,
15951 included_columns,
15952 partial_predicate,
15953 extra_columns: extra_columns.clone(),
15954 expression,
15955 is_unique,
15956 opclass,
15957 method_name,
15958 }))
15959 }
15960
15961 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15962 /// column-level `REFERENCES ...` clause. The trailing FK is
15963 /// normalised into table-level shape (single-element columns +
15964 /// parent_columns) so the engine sees one uniform constraint list.
15965 fn parse_column_def_with_fk(
15966 &mut self,
15967 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15968 let col = self.parse_column_def()?;
15969 // v7.39 (round 308, V29) — an explicitly named inline FK:
15970 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15971 // loop leaves this spelling intact precisely so the name can be
15972 // kept here; PG reports it in violation messages and matches it
15973 // in `SET CONSTRAINTS`.
15974 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15975 {
15976 self.advance();
15977 Some(self.expect_ident_like()?)
15978 } else {
15979 None
15980 };
15981 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15982 let inline_references = matches!(
15983 self.peek(),
15984 Token::Ident(s) if s.eq_ignore_ascii_case("references")
15985 );
15986 if !inline_references {
15987 return Ok((col, None));
15988 }
15989 let (
15990 parent_table,
15991 parent_columns,
15992 on_delete,
15993 on_update,
15994 match_type,
15995 deferrable,
15996 initially_deferred,
15997 ) = self.parse_references_tail(1)?;
15998 let fk = ForeignKeyConstraint {
15999 name: declared_name,
16000 columns: vec![col.name.clone()],
16001 parent_table,
16002 parent_columns,
16003 on_delete,
16004 on_update,
16005 match_type,
16006 deferrable,
16007 initially_deferred,
16008 };
16009 Ok((col, Some(fk)))
16010 }
16011
16012 /// v7.13.0 — parse a column type (consuming the type ident and
16013 /// any trailing parameters / `[]`), without surrounding column
16014 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16015 /// Returns the resolved `ColumnTypeName` plus implied
16016 /// `(auto_increment, not_null)` flags from PG SERIAL family
16017 /// shorthands — callers that don't expect those (ALTER COLUMN
16018 /// TYPE) can discard them.
16019 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16020 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16021 Ok(ty)
16022 }
16023
16024 #[allow(clippy::type_complexity)]
16025 fn parse_type_with_implied_flags(
16026 &mut self,
16027 ) -> Result<
16028 (
16029 ColumnTypeName,
16030 bool,
16031 bool,
16032 Option<String>,
16033 Collation,
16034 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16035 bool,
16036 // v7.39 (round 676) — the collation NAME as written, which the
16037 // `Collation` enum above cannot carry.
16038 Option<String>,
16039 bool,
16040 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16041 // list captured at type-parse time. None for all
16042 // non-ENUM types.
16043 Option<Vec<String>>,
16044 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16045 // list. Distinct from ENUM (subset semantics).
16046 Option<Vec<String>>,
16047 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16048 // width, lost when the type collapses to SmallInt / Int.
16049 Option<MysqlIntWidth>,
16050 // v7.39 (round 424) — declared fractional-seconds precision of a
16051 // MySQL temporal column (bare spelling = 0). None under PG.
16052 Option<u8>,
16053 ),
16054 ParseError,
16055 > {
16056 let mut ty_ident = match self.advance() {
16057 Token::Ident(s) => s,
16058 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16059 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16060 // '<span>'` literal grammar. As a column type it lands
16061 // here directly; downstream resolution still uses the
16062 // canonical lowercase string.
16063 Token::Interval => "interval".to_string(),
16064 other => {
16065 return Err(ParseError {
16066 message: format!("expected column type, got {other:?}"),
16067 token_pos: self.consumed_pos(),
16068 });
16069 }
16070 };
16071 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16072 // pg_dump qualifies extension types (`public.vector(1024)`).
16073 // SPG is single-namespace; drop the schema and resolve the
16074 // bare type — same treatment table names already get.
16075 while matches!(self.peek(), Token::Dot) {
16076 self.advance();
16077 ty_ident = self.expect_ident_like()?;
16078 }
16079 let mut implied_auto_increment = false;
16080 let mut implied_not_null = false;
16081 let mut user_type_ref: Option<String> = None;
16082 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16083 // value list, captured here and bubbled up through the
16084 // ColumnDef so the engine can attach it to the column
16085 // schema (and validate INSERT cells against it).
16086 let mut inline_enum_variants: Option<Vec<String>> = None;
16087 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16088 let mut inline_set_variants: Option<Vec<String>> = None;
16089 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16090 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16091 // collapses to SmallInt / Int. Only under the MySQL dialect.
16092 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16093 // v7.39 (round 424) — the declared fractional-seconds precision of a
16094 // MySQL temporal column. Set by the temporal arms below; stays None
16095 // for PG (whose temporal columns keep full microseconds).
16096 let mut mysql_fsp: Option<u8> = None;
16097 let mut ty = match ty_ident.as_str() {
16098 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16099 "smallserial" | "serial2" => {
16100 implied_auto_increment = true;
16101 implied_not_null = true;
16102 ColumnTypeName::SmallInt
16103 }
16104 "serial" | "serial4" => {
16105 implied_auto_increment = true;
16106 implied_not_null = true;
16107 ColumnTypeName::Int
16108 }
16109 "bigserial" | "serial8" => {
16110 implied_auto_increment = true;
16111 implied_not_null = true;
16112 ColumnTypeName::BigInt
16113 }
16114 // MySQL flavours we accept by aliasing to the closest SPG
16115 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16116 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16117 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16118 // without semantic effect.
16119 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16120 // PG's internal type names; pg_dump and hand-written PG schemas
16121 // use them interchangeably with smallint / int / bigint (the cast
16122 // path already accepted them, only the column grammar didn't).
16123 "smallint" | "int2" => {
16124 // v7.14.0 — MySQL display-width on integers
16125 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16126 // parenthesised number is purely cosmetic — it
16127 // doesn't change storage. Accept + discard.
16128 self.consume_optional_paren_size();
16129 ColumnTypeName::SmallInt
16130 }
16131 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16132 // canonical encoding for BOOLEAN. Every MySQL driver
16133 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16134 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16135 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16136 // gave the customer i16-shaped values where the app
16137 // expected bool — a Tier-A silent type drift on
16138 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16139 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16140 // stay SmallInt (the legacy width-agnostic path).
16141 "tinyint" => {
16142 let width = self.peek_optional_paren_size_value();
16143 self.consume_optional_paren_size();
16144 if width == Some(1) {
16145 ColumnTypeName::Bool
16146 } else {
16147 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16148 // lost width so the write path can enforce -128..127.
16149 if self.mysql_dialect {
16150 mysql_int_width = Some(MysqlIntWidth::Tiny);
16151 }
16152 ColumnTypeName::SmallInt
16153 }
16154 }
16155 "mediumint" => {
16156 self.consume_optional_paren_size();
16157 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16158 if self.mysql_dialect {
16159 mysql_int_width = Some(MysqlIntWidth::Medium);
16160 }
16161 ColumnTypeName::Int
16162 }
16163 "int" | "integer" | "int4" => {
16164 self.consume_optional_paren_size();
16165 ColumnTypeName::Int
16166 }
16167 "bigint" | "int8" => {
16168 self.consume_optional_paren_size();
16169 ColumnTypeName::BigInt
16170 }
16171 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16172 // (mailrs round-5 G6). Consume the optional `PRECISION`
16173 // tail when the type keyword was `double` / `DOUBLE`.
16174 //
16175 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16176 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16177 // p in 1..=24 is real, 25..=53 is double precision, and
16178 // anything else is an error.
16179 "float" | "double" | "real" => {
16180 if ty_ident.eq_ignore_ascii_case("double")
16181 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16182 {
16183 self.advance();
16184 }
16185 if ty_ident.eq_ignore_ascii_case("real") {
16186 // v7.39 (round 274) — the two dialects genuinely
16187 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16188 // synonym for DOUBLE (8-byte). Round 269 made REAL
16189 // 32-bit globally and thereby narrowed the stored
16190 // precision of every MySQL REAL column.
16191 if self.mysql_dialect {
16192 ColumnTypeName::Float
16193 } else {
16194 ColumnTypeName::Real
16195 }
16196 } else if ty_ident.eq_ignore_ascii_case("float")
16197 && self.mysql_dialect
16198 && matches!(self.peek(), Token::LParen)
16199 && self.peek_paren_has_comma()
16200 {
16201 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16202 // display form (`FLOAT(10,2)`), which PG has no
16203 // equivalent of. It was `syntax error at or near ","`,
16204 // so the whole CREATE failed. The digits are a display
16205 // hint only; SPG stores the full double.
16206 self.consume_optional_paren_size();
16207 ColumnTypeName::Float
16208 } else if ty_ident.eq_ignore_ascii_case("float")
16209 && matches!(self.peek(), Token::LParen)
16210 {
16211 // PG words the two bounds differently, and
16212 // parse_paren_size already rejects a zero.
16213 let p = self.parse_paren_size("FLOAT")?;
16214 if p > 53 {
16215 return Err(self.err(String::from(
16216 "precision for type float must be less than 54 bits",
16217 )));
16218 }
16219 if p <= 24 {
16220 ColumnTypeName::Real
16221 } else {
16222 ColumnTypeName::Float
16223 }
16224 } else {
16225 ColumnTypeName::Float
16226 }
16227 }
16228 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16229 "float4" => ColumnTypeName::Real,
16230 "float8" => ColumnTypeName::Float,
16231 "text" => ColumnTypeName::Text,
16232 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16233 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16234 // real MySQL schema and NONE of them existed: the CREATE
16235 // failed outright with `type "blob" does not exist`, so the
16236 // table was never made. The sizes differ only in MySQL's
16237 // maximum length, which SPG does not cap, so they collapse
16238 // onto TEXT and BYTEA the way the unsized spellings do.
16239 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16240 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16241 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16242 // enforce, consumed so the declaration parses.
16243 "varbinary" | "binary" => {
16244 self.consume_optional_paren_size();
16245 ColumnTypeName::Bytes
16246 }
16247 "name" => ColumnTypeName::Name,
16248 "xid" => ColumnTypeName::Xid,
16249 "oid" => ColumnTypeName::Oid,
16250 "xid8" => ColumnTypeName::Xid8,
16251 "bool" | "boolean" => ColumnTypeName::Bool,
16252 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16253 // an unbounded `character varying`, which the arm below has always
16254 // read as text. Only the short spelling demanded a length, so
16255 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16256 // there is — failed on `VARCHAR type requires (N)` while the long
16257 // spelling of the same thing was accepted. The same asymmetry
16258 // round 613 closed on the CAST side, here on the DDL side.
16259 "varchar" => {
16260 if matches!(self.peek(), Token::LParen) {
16261 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16262 } else {
16263 ColumnTypeName::Text
16264 }
16265 }
16266 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16267 // `character` below (SQL standard).
16268 "char" => {
16269 if matches!(self.peek(), Token::LParen) {
16270 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16271 } else {
16272 ColumnTypeName::Char(1)
16273 }
16274 }
16275 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16276 // `character(n)` = char, bare `character` = char(1). Unbounded
16277 // `character varying` maps to text.
16278 "character" => {
16279 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16280 self.advance();
16281 if matches!(self.peek(), Token::LParen) {
16282 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16283 } else {
16284 ColumnTypeName::Text
16285 }
16286 } else if matches!(self.peek(), Token::LParen) {
16287 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16288 } else {
16289 ColumnTypeName::Char(1)
16290 }
16291 }
16292 "vector" => {
16293 let dim = self.parse_paren_size("VECTOR")?;
16294 let encoding = self.parse_optional_vector_encoding()?;
16295 ColumnTypeName::Vector { dim, encoding }
16296 }
16297 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16298 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16299 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16300 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16301 // DECIMAL(10,2))` — how nearly every money column is written,
16302 // in either dialect — was a syntax error and the table was
16303 // never created. `FIXED` is MySQL's alias alone, so it is
16304 // taken only in that dialect.
16305 "numeric" | "decimal" | "dec" => {
16306 let (precision, scale) = self.parse_optional_numeric_params()?;
16307 ColumnTypeName::Numeric(precision, scale)
16308 }
16309 "fixed" if self.mysql_dialect => {
16310 let (precision, scale) = self.parse_optional_numeric_params()?;
16311 ColumnTypeName::Numeric(precision, scale)
16312 }
16313 "date" => ColumnTypeName::Date,
16314 // MySQL's `DATETIME` is the same domain as standard
16315 // `TIMESTAMP` — accept both spellings.
16316 "timestamp" | "datetime" => {
16317 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16318 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16319 // TIME ZONE` clause, so consume it first.
16320 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16321 // (it truncates on write and pads on render), so capture it;
16322 // a bare spelling means precision 0 there. PG stores µs always
16323 // and keeps `None`.
16324 let n = self.take_optional_paren_size();
16325 if self.mysql_dialect {
16326 mysql_fsp = Some(n.unwrap_or(0).min(6));
16327 }
16328 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16329 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16330 // the full form. SPG canonicalises:
16331 // - WITH TIME ZONE → Timestamptz
16332 // - WITHOUT TIME ZONE → Timestamp
16333 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16334 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16335 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16336 {
16337 self.advance(); // WITH
16338 self.advance(); // TIME
16339 self.advance(); // ZONE
16340 ColumnTypeName::Timestamptz
16341 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16342 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16343 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16344 {
16345 self.advance(); // WITHOUT
16346 self.advance(); // TIME
16347 self.advance(); // ZONE
16348 ColumnTypeName::Timestamp
16349 } else {
16350 // A second `(precision)` cannot legally follow, but the
16351 // old grammar tolerated it; keep that tolerance.
16352 self.consume_optional_paren_size();
16353 ColumnTypeName::Timestamp
16354 }
16355 }
16356 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16357 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16358 // only PG-wire OID differs.
16359 "timestamptz" => {
16360 self.consume_optional_paren_size();
16361 ColumnTypeName::Timestamptz
16362 }
16363 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16364 // validation. We accept the JSONB spelling too because
16365 // most PG clients default to it; SPG doesn't distinguish
16366 // the two (no path-operator perf advantage to model).
16367 "json" => ColumnTypeName::Json,
16368 "jsonb" => ColumnTypeName::Jsonb,
16369 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16370 // surface here. Same storage shape; mapping happens at
16371 // the engine side via the ColumnTypeName → DataType
16372 // resolver. Literal forms are handled at coerce_value
16373 // time so the lexer stays untouched.
16374 "bytea" | "bytes" => ColumnTypeName::Bytes,
16375 // v7.17.0 Phase 7 — PG network address types
16376 // v7.17.0 had a Text-backed fallback here for
16377 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16378 // each to a first-class type; the keywords are
16379 // bound below in the ζ-A block.
16380 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16381 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16382 // arrives in v7.12.1+; the type itself loads here so
16383 // mailrs's `scripts/init-schema.sql` runs unmodified.
16384 "tsvector" => ColumnTypeName::TsVector,
16385 "tsquery" => ColumnTypeName::TsQuery,
16386 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16387 // surface for Django / Rails / Hibernate's default
16388 // PK pattern.
16389 "uuid" => ColumnTypeName::Uuid,
16390 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16391 // Storage = three-field {months, days, micros}, catalog
16392 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16393 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16394 "interval" => {
16395 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16396 // SECOND` and an optional `(p)` precision. SPG stores the full
16397 // {months,days,micros}; consume + ignore the qualifier/precision.
16398 while matches!(self.peek(), Token::To)
16399 || matches!(self.peek(), Token::Ident(s) if matches!(
16400 s.to_ascii_lowercase().as_str(),
16401 "year" | "month" | "day" | "hour" | "minute" | "second"
16402 ))
16403 {
16404 self.advance();
16405 }
16406 self.consume_optional_paren_size();
16407 ColumnTypeName::Interval
16408 }
16409 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16410 // i64 microseconds since 00:00:00. Wire OID 1083.
16411 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16412 "time" => {
16413 // v7.39 (round 424) — MySQL TIME carries a semantic
16414 // fractional-seconds precision, bare meaning 0.
16415 let n = self.take_optional_paren_size();
16416 if self.mysql_dialect {
16417 mysql_fsp = Some(n.unwrap_or(0).min(6));
16418 }
16419 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16420 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16421 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16422 {
16423 self.advance();
16424 self.advance();
16425 self.advance();
16426 ColumnTypeName::TimeTz
16427 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16428 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16429 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16430 {
16431 self.advance();
16432 self.advance();
16433 self.advance();
16434 ColumnTypeName::Time
16435 } else {
16436 ColumnTypeName::Time
16437 }
16438 }
16439 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16440 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16441 "year" => ColumnTypeName::Year,
16442 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16443 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16444 "timetz" => ColumnTypeName::TimeTz,
16445 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16446 // Wire OID 790.
16447 "money" => ColumnTypeName::Money,
16448 // v7.17.0 Phase 3.P0-38 — PG range types.
16449 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16450 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16451 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16452 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16453 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16454 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16455 // v7.37.5 δ — PG 14+ multirange keywords.
16456 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16457 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16458 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16459 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16460 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16461 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16462 // v7.37.5 ε — PG geometry scalar keywords.
16463 "point" => ColumnTypeName::Point,
16464 "lseg" => ColumnTypeName::Lseg,
16465 "path" => ColumnTypeName::Path,
16466 "box" => ColumnTypeName::PgBox,
16467 "polygon" => ColumnTypeName::Polygon,
16468 "line" => ColumnTypeName::Line,
16469 "circle" => ColumnTypeName::Circle,
16470 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16471 "inet" => ColumnTypeName::Inet,
16472 "cidr" => ColumnTypeName::Cidr,
16473 "macaddr" => ColumnTypeName::Macaddr,
16474 "macaddr8" => ColumnTypeName::Macaddr8,
16475 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16476 // width in the value, so the optional `(N)` typmod is accepted and
16477 // ignored (the column stores whatever width it's given).
16478 "bit" => {
16479 let varying = matches!(
16480 self.peek(),
16481 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16482 );
16483 if varying {
16484 self.advance();
16485 }
16486 // v7.39 (round 281) — the length used to be parsed and
16487 // dropped, so `bit(3)` accepted a five-bit string.
16488 let n = if matches!(self.peek(), Token::LParen) {
16489 self.parse_paren_size("BIT")?
16490 } else {
16491 0
16492 };
16493 if varying {
16494 ColumnTypeName::BitVarying(n)
16495 } else {
16496 ColumnTypeName::Bit(n)
16497 }
16498 }
16499 "varbit" => {
16500 let n = if matches!(self.peek(), Token::LParen) {
16501 self.parse_paren_size("VARBIT")?
16502 } else {
16503 0
16504 };
16505 ColumnTypeName::BitVarying(n)
16506 }
16507 "xml" => ColumnTypeName::Xml,
16508 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16509 "hstore" => ColumnTypeName::Hstore,
16510 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16511 // `ENUM('a','b','c')`. Storage is TEXT; the value
16512 // list lands on `inline_enum_variants` for the
16513 // engine to validate INSERT cells against. Empty
16514 // value list is a parse error (matches MySQL).
16515 "enum" => {
16516 // Expect the opening `(`.
16517 if !matches!(self.peek(), Token::LParen) {
16518 return Err(self.err(alloc::format!(
16519 "expected '(' after ENUM, got {:?}",
16520 self.peek()
16521 )));
16522 }
16523 self.advance();
16524 let mut variants: Vec<String> = Vec::new();
16525 loop {
16526 match self.advance() {
16527 Token::String(s) => variants.push(s),
16528 other => {
16529 return Err(self.err(alloc::format!(
16530 "ENUM(...) expects string literal variants, got {other:?}"
16531 )));
16532 }
16533 }
16534 match self.peek() {
16535 Token::Comma => {
16536 self.advance();
16537 continue;
16538 }
16539 Token::RParen => {
16540 self.advance();
16541 break;
16542 }
16543 other => {
16544 return Err(self.err(alloc::format!(
16545 "expected ',' or ')' in ENUM(...), got {other:?}"
16546 )));
16547 }
16548 }
16549 }
16550 if variants.is_empty() {
16551 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16552 }
16553 inline_enum_variants = Some(variants);
16554 // Storage is plain TEXT; the variant list lives on
16555 // the ColumnSchema side.
16556 ColumnTypeName::Text
16557 }
16558 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16559 // `SET('a','b','c')`. Same parse shape as ENUM;
16560 // semantics differ (subset rather than pick-one).
16561 "set" => {
16562 if !matches!(self.peek(), Token::LParen) {
16563 return Err(self.err(alloc::format!(
16564 "expected '(' after SET, got {:?}",
16565 self.peek()
16566 )));
16567 }
16568 self.advance();
16569 let mut variants: Vec<String> = Vec::new();
16570 loop {
16571 match self.advance() {
16572 Token::String(s) => variants.push(s),
16573 other => {
16574 return Err(self.err(alloc::format!(
16575 "SET(...) expects string literal variants, got {other:?}"
16576 )));
16577 }
16578 }
16579 match self.peek() {
16580 Token::Comma => {
16581 self.advance();
16582 continue;
16583 }
16584 Token::RParen => {
16585 self.advance();
16586 break;
16587 }
16588 other => {
16589 return Err(self.err(alloc::format!(
16590 "expected ',' or ')' in SET(...), got {other:?}"
16591 )));
16592 }
16593 }
16594 }
16595 if variants.is_empty() {
16596 return Err(self.err("SET(...) must declare at least one variant".into()));
16597 }
16598 inline_set_variants = Some(variants);
16599 ColumnTypeName::Text
16600 }
16601 _other => {
16602 // v7.17.0 Phase 1.4 — unknown ident → defer
16603 // resolution to the engine. Stored as Text in
16604 // ColumnTypeName + the original name carried as
16605 // `user_type_ref` so CREATE TABLE can look up
16606 // user-defined enum / domain types.
16607 user_type_ref = Some(ty_ident.clone());
16608 ColumnTypeName::Text
16609 }
16610 };
16611 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16612 // right after the type keyword. Pre-4.4 SPG consumed +
16613 // discarded the keyword, leaving a customer column
16614 // declared `id INT UNSIGNED NOT NULL` silently accepting
16615 // negative values — a Tier-A correctness drift where
16616 // application invariants (auto-increment-IDs never
16617 // negative) silently broke on cutover. Now: capture as
16618 // a column flag, persist on the schema, enforce at
16619 // INSERT / UPDATE time.
16620 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16621 {
16622 self.advance();
16623 true
16624 } else {
16625 false
16626 };
16627 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16628 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16629 // stores text as UTF-8 always so CHARACTER SET is still a
16630 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16631 // name: it gets classified into a `Collation` variant the
16632 // engine consults at WHERE-eval time. PG `default` /
16633 // `pg_catalog.default` / `C` / `POSIX` collations all
16634 // resolve to `Binary` (the prior behaviour); `_ci` /
16635 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16636 // The schema-qualifier form (`pg_catalog.default`) lexes
16637 // as `Ident '.' Ident` — peek for the `.` and consume both
16638 // halves so it's treated as one collation name. PG's
16639 // `IDENT.IDENT` collation form (which can appear here) is
16640 // resolved by Collation::from_collation_name on the bare
16641 // identifier after the dot.
16642 let mut collation = Collation::Binary;
16643 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16644 // clause was written. The engine needs this to tell an explicit
16645 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16646 // clause at all: both resolve to `Collation::Binary`, but under the
16647 // MySQL dialect the latter takes the folding default collation.
16648 let mut collation_explicit = false;
16649 let mut collation_name: Option<alloc::string::String> = None;
16650 loop {
16651 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16652 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16653 {
16654 self.advance(); // CHARACTER
16655 self.advance(); // SET
16656 if matches!(
16657 self.peek(),
16658 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16659 ) {
16660 self.advance();
16661 }
16662 continue;
16663 }
16664 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16665 self.advance(); // COLLATE
16666 // Accept Ident / QuotedIdent / String AND the
16667 // keyword-tokenised `Default` (PG `pg_catalog.default`
16668 // and bare `DEFAULT` collation names — `default` is a
16669 // reserved word so the lexer hands back Token::Default
16670 // not Token::Ident).
16671 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16672 match this.peek().clone() {
16673 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16674 this.advance();
16675 Some(s)
16676 }
16677 Token::Default => {
16678 this.advance();
16679 Some(alloc::string::String::from("default"))
16680 }
16681 _ => None,
16682 }
16683 };
16684 let raw = if let Some(head) = read_collation_atom(self) {
16685 // Schema-qualified PG form: `pg_catalog.default`.
16686 if matches!(self.peek(), Token::Dot) {
16687 self.advance();
16688 let tail = read_collation_atom(self).unwrap_or_default();
16689 alloc::format!("{head}.{tail}")
16690 } else {
16691 head
16692 }
16693 } else {
16694 alloc::string::String::new()
16695 };
16696 if !raw.is_empty() {
16697 collation_explicit = true;
16698 // v7.39 (round 676) — keep the name too. The enum below
16699 // folds C / POSIX / en_US / default into one value, and
16700 // `pg_attribute.attcollation` has to tell them apart.
16701 // The schema qualifier goes: PG's `pg_catalog.default`
16702 // and a bare `default` name the same collation.
16703 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16704 // encoding suffix. Round 676 used `rsplit('.')` for
16705 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16706 // PG writes `pg_catalog.default` (qualifier) and
16707 // `en_US.utf8` (locale + encoding) with the same
16708 // separator. Only `pg_catalog.` is a qualifier, and it
16709 // is the only one PG's own dumps emit.
16710 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16711 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16712 collation_name = Some(alloc::string::String::from(bare));
16713 let parsed = Collation::from_collation_name(&raw);
16714 // Last COLLATE clause wins, but `Binary` from a
16715 // bare keyword like `default` should not
16716 // silently downgrade a stronger one set earlier
16717 // on the same column. v7.17 only ships one
16718 // non-Binary variant so a simple OR is enough.
16719 if parsed != Collation::Binary {
16720 collation = parsed;
16721 }
16722 }
16723 continue;
16724 }
16725 break;
16726 }
16727 // v7.10.10 — postfix `[]` widens the base type to its array
16728 // type. PG accepts `TYPE[]` after any base type and so does
16729 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16730 // all through; the old "only TEXT[]" note was stale).
16731 if matches!(self.peek(), Token::LBracket) {
16732 self.advance();
16733 if !matches!(self.peek(), Token::RBracket) {
16734 return Err(self.err(alloc::format!(
16735 "TEXT[] takes no dimension; got {:?}",
16736 self.peek()
16737 )));
16738 }
16739 self.advance();
16740 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16741 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16742 // still error here.
16743 ty = match ty {
16744 ColumnTypeName::Text => ColumnTypeName::TextArray,
16745 ColumnTypeName::Int => ColumnTypeName::IntArray,
16746 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16747 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16748 // `[]` grammar. Wire OID 1187.
16749 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16750 // v7.37.5 γ — full PG array-of-scalar family.
16751 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16752 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16753 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16754 // NUMERIC(p, s) loses its precision params at the
16755 // array level (matches PG: `NUMERIC[]` is untyped,
16756 // per-element precision flows through values).
16757 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16758 ColumnTypeName::Date => ColumnTypeName::DateArray,
16759 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16760 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16761 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16762 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16763 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16764 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16765 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16766 // the array level (matches PG semantics where the
16767 // element precision is per-row, not column-wide).
16768 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16769 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16770 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16771 // follow-up.
16772 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16773 other => {
16774 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16775 }
16776 };
16777 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16778 // for INT/TEXT/BIGINT. Anything else is an error.
16779 if matches!(self.peek(), Token::LBracket) {
16780 self.advance();
16781 if !matches!(self.peek(), Token::RBracket) {
16782 return Err(self.err(alloc::format!(
16783 "TYPE[][] second dimension takes no size; got {:?}",
16784 self.peek()
16785 )));
16786 }
16787 self.advance();
16788 ty = match ty {
16789 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16790 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16791 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16792 // v7.39 (read01 round 75) — bool[][].
16793 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16794 other => {
16795 return Err(self.err(alloc::format!(
16796 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16797 TEXT[][] only; got {other:?}"
16798 )));
16799 }
16800 };
16801 }
16802 }
16803 Ok((
16804 ty,
16805 implied_auto_increment,
16806 implied_not_null,
16807 user_type_ref,
16808 collation,
16809 collation_explicit,
16810 collation_name,
16811 is_unsigned,
16812 inline_enum_variants,
16813 inline_set_variants,
16814 mysql_int_width,
16815 mysql_fsp,
16816 ))
16817 }
16818
16819 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16820 // v7.20 — PG reserves the table-constraint keywords, so a
16821 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16822 // malformed constraint clause (e.g. `UNIQUE a` missing its
16823 // parens), not a column named "unique". Since v7.17's
16824 // unknown-type leniency (`user_type_ref`) such a clause
16825 // would otherwise parse as a column with a user-defined
16826 // type — silently accepting invalid DDL. Quoted
16827 // identifiers ("unique" / `unique`) remain valid names.
16828 if let Token::Ident(s) = self.peek()
16829 && [
16830 "unique",
16831 "primary",
16832 "foreign",
16833 "constraint",
16834 "check",
16835 "references",
16836 "exclude",
16837 ]
16838 .iter()
16839 .any(|kw| s.eq_ignore_ascii_case(kw))
16840 {
16841 return Err(self.err(alloc::format!(
16842 "unexpected reserved keyword '{s}' at start of column definition \
16843 (malformed table constraint?)"
16844 )));
16845 }
16846 let name = self.expect_ident_like()?;
16847 let (
16848 ty,
16849 implied_auto_increment,
16850 implied_not_null,
16851 user_type_ref,
16852 collation,
16853 collation_explicit,
16854 collation_name,
16855 is_unsigned,
16856 inline_enum_variants,
16857 inline_set_variants,
16858 mysql_int_width,
16859 mysql_fsp,
16860 ) = self.parse_type_with_implied_flags()?;
16861 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16862 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16863 // each at most once.
16864 let mut default: Option<Expr> = None;
16865 let mut nullable = !implied_not_null;
16866 let mut nullability_seen = implied_not_null;
16867 let mut auto_increment = implied_auto_increment;
16868 let mut is_primary_key = false;
16869 let mut is_unique = false;
16870 let mut unique_nulls_not_distinct = false;
16871 let mut constraint_deferrable = false;
16872 let mut constraint_initially_deferred = false;
16873 let mut check: Option<Expr> = None;
16874 let mut on_update_runtime: Option<Expr> = None;
16875 let mut generated_stored_expr: Option<Box<Expr>> = None;
16876 let mut identity_always = false;
16877 loop {
16878 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16879 // not-null constraints by name and pg_dump emits them
16880 // inline: `id bigint CONSTRAINT contacts_id_not_null1
16881 // NOT NULL`. Accept and discard the name; whatever
16882 // constraint follows is parsed by the arms below.
16883 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16884 // v7.39 (round 308, V29) — a name on an inline
16885 // REFERENCES belongs to the FOREIGN KEY, and the caller
16886 // (`parse_column_def_with_fk`) is what builds it, so
16887 // leave the whole clause for it. Dropping the name here
16888 // is what made `CONSTRAINT fk_a REFERENCES …` come back
16889 // as the synthesised `c_pid_fkey` — which then could
16890 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16891 // `advance()` takes tokens by `mem::replace`, so there
16892 // is no rewinding once consumed.
16893 if matches!(
16894 self.tokens.get(self.pos + 2),
16895 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16896 ) {
16897 break;
16898 }
16899 self.advance();
16900 let _name = self.expect_ident_like()?;
16901 continue;
16902 }
16903 // v7.39 (round 379) — MySQL's SHORT generated-column form
16904 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16905 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16906 // below), but hand-written schemas and app migrations use this.
16907 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16908 // SPG computes-and-stores either way, like the long form.
16909 if matches!(self.peek(), Token::As) {
16910 self.advance();
16911 if !matches!(self.peek(), Token::LParen) {
16912 return Err(self.err(alloc::format!(
16913 "expected '(' after AS in a generated column, got {:?}",
16914 self.peek()
16915 )));
16916 }
16917 self.advance();
16918 let expr = self.parse_expr(0)?;
16919 if !matches!(self.peek(), Token::RParen) {
16920 return Err(self.err(alloc::format!(
16921 "expected ')' after AS (<expr>), got {:?}",
16922 self.peek()
16923 )));
16924 }
16925 self.advance();
16926 if matches!(self.peek(), Token::Ident(s)
16927 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16928 {
16929 self.advance();
16930 }
16931 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16932 continue;
16933 }
16934 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16935 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16936 // the modern replacement for SERIAL in hand-written
16937 // schemas). Both flavours map onto the auto-increment
16938 // machinery — SPG's serial semantics ≈ BY DEFAULT;
16939 // ALWAYS's reject-explicit-values nuance is documented
16940 // leniency. Generated EXPRESSION columns
16941 // (`AS (expr) STORED`) are not supported: error loudly
16942 // instead of silently storing NULLs.
16943 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16944 self.advance();
16945 let mut saw_generated_always = false;
16946 match self.peek().clone() {
16947 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16948 self.advance();
16949 saw_generated_always = true;
16950 }
16951 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16952 self.advance();
16953 if !matches!(self.peek(), Token::Default) {
16954 return Err(self.err(alloc::format!(
16955 "expected DEFAULT after GENERATED BY, got {:?}",
16956 self.peek()
16957 )));
16958 }
16959 self.advance();
16960 }
16961 other => {
16962 return Err(self.err(alloc::format!(
16963 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16964 )));
16965 }
16966 }
16967 if !matches!(self.peek(), Token::As) {
16968 return Err(self.err(alloc::format!(
16969 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16970 self.peek()
16971 )));
16972 }
16973 self.advance();
16974 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16975 // ( <expr> ) STORED` stored computed-column. The
16976 // expression is captured for the engine to recompute
16977 // on every INSERT / UPDATE. v7.37.7 accepts the
16978 // STORED keyword only; PG also has VIRTUAL, which
16979 // v7.37.7 carves out (sentori only uses STORED).
16980 if matches!(self.peek(), Token::LParen) {
16981 self.advance();
16982 let expr = self.parse_expr(0)?;
16983 if !matches!(self.peek(), Token::RParen) {
16984 return Err(self.err(alloc::format!(
16985 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16986 self.peek()
16987 )));
16988 }
16989 self.advance();
16990 let stored = match self.peek() {
16991 Token::Ident(s) | Token::QuotedIdent(s)
16992 if s.eq_ignore_ascii_case("stored") =>
16993 {
16994 self.advance();
16995 true
16996 }
16997 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
16998 // generated columns. SPG computes them on write and
16999 // persists like STORED; the two are observably
17000 // identical for query results (the value, recompute
17001 // on base-column change, and NOT NULL enforcement all
17002 // match), so a PG 18 schema/dump using VIRTUAL loads
17003 // and behaves correctly. The compute-on-read storage
17004 // saving is an invisible internal difference.
17005 Token::Ident(s) | Token::QuotedIdent(s)
17006 if s.eq_ignore_ascii_case("virtual") =>
17007 {
17008 self.advance();
17009 false
17010 }
17011 other => {
17012 return Err(self.err(alloc::format!(
17013 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17014 got {other:?}"
17015 )));
17016 }
17017 };
17018 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17019 generated_stored_expr = Some(Box::new(expr));
17020 continue;
17021 }
17022 self.expect_keyword_ident("identity")?;
17023 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17024 // consume the balanced parens and discard (SPG's
17025 // auto-increment is max+1-scan based).
17026 if matches!(self.peek(), Token::LParen) {
17027 let mut depth = 0usize;
17028 loop {
17029 match self.advance() {
17030 Token::LParen => depth += 1,
17031 Token::RParen => {
17032 depth -= 1;
17033 if depth == 0 {
17034 break;
17035 }
17036 }
17037 Token::Eof => {
17038 return Err(self.err(
17039 "unterminated sequence-options parens after IDENTITY".into(),
17040 ));
17041 }
17042 _ => {}
17043 }
17044 }
17045 }
17046 auto_increment = true;
17047 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17048 // can reject explicit non-DEFAULT INSERT values (unless
17049 // OVERRIDING SYSTEM VALUE) the way PG does.
17050 identity_always = saw_generated_always;
17051 // PG identity columns are implicitly NOT NULL.
17052 nullable = false;
17053 continue;
17054 }
17055 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17056 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17057 // is accepted today. The "ON" token is an Ident
17058 // (not reserved) — peek before consuming.
17059 if matches!(self.peek(), Token::On)
17060 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17061 {
17062 self.advance(); // ON
17063 self.advance(); // update
17064 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17065 let next = self.peek().clone();
17066 match next {
17067 Token::Ident(s) | Token::QuotedIdent(s)
17068 if s.eq_ignore_ascii_case("current_timestamp") =>
17069 {
17070 self.advance();
17071 // Optional `(N)` precision.
17072 if matches!(self.peek(), Token::LParen) {
17073 self.advance();
17074 if !matches!(self.peek(), Token::Integer(_)) {
17075 return Err(self.err(alloc::format!(
17076 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17077 self.peek()
17078 )));
17079 }
17080 self.advance();
17081 if !matches!(self.peek(), Token::RParen) {
17082 return Err(self.err(alloc::format!(
17083 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17084 self.peek()
17085 )));
17086 }
17087 self.advance();
17088 }
17089 on_update_runtime = Some(Expr::FunctionCall {
17090 name: "now".into(),
17091 args: Vec::new(),
17092 });
17093 continue;
17094 }
17095 other => {
17096 return Err(self.err(alloc::format!(
17097 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17098 )));
17099 }
17100 }
17101 }
17102 if matches!(self.peek(), Token::Default) {
17103 if default.is_some() {
17104 return Err(self.err("DEFAULT specified twice".into()));
17105 }
17106 self.advance();
17107 default = Some(self.parse_expr(0)?);
17108 continue;
17109 }
17110 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17111 // token with NOT NULL and sits EARLIER in the loop than the
17112 // deferrability arm, so without the lookahead it was reported as
17113 // "NOT NULL specified twice" (or "expected NULL after NOT").
17114 if matches!(self.peek(), Token::Not)
17115 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17116 {
17117 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17118 self.consume_optional_deferrable_clauses()?;
17119 continue;
17120 }
17121 if matches!(self.peek(), Token::Not) {
17122 if nullability_seen {
17123 return Err(self.err("NOT NULL specified twice".into()));
17124 }
17125 self.advance();
17126 if !matches!(self.peek(), Token::Null) {
17127 return Err(self.err(format!(
17128 "expected NULL after NOT in column def, got {:?}",
17129 self.peek()
17130 )));
17131 }
17132 self.advance();
17133 nullable = false;
17134 nullability_seen = true;
17135 continue;
17136 }
17137 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17138 // "this column is nullable" marker (the default in
17139 // standard SQL anyway). mysqldump emits it routinely
17140 // (`col TYPE NULL DEFAULT NULL` for nullable
17141 // timestamps etc). Accept + no-op.
17142 if matches!(self.peek(), Token::Null) {
17143 if nullability_seen && !nullable {
17144 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17145 // sentence, PG18-measured (the table name is the
17146 // caller's; the column half is exact).
17147 return Err(self.err(alloc::format!(
17148 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17149 )));
17150 }
17151 self.advance();
17152 nullable = true;
17153 nullability_seen = true;
17154 continue;
17155 }
17156 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17157 // arrives as a bare Ident. Match either, case-insensitive.
17158 if let Token::Ident(s) = self.peek()
17159 && (s.eq_ignore_ascii_case("auto_increment")
17160 || s.eq_ignore_ascii_case("autoincrement"))
17161 {
17162 if auto_increment {
17163 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17164 }
17165 self.advance();
17166 auto_increment = true;
17167 continue;
17168 }
17169 // v7.9.13 — inline `PRIMARY KEY` column constraint
17170 // (mailrs F1). Implies `NOT NULL`. The engine creates
17171 // a BTree index for the PK column at CREATE TABLE time
17172 // so FK parent-side index lookups resolve.
17173 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17174 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17175 // spelling was a parse error, so a pg_dump carrying one stopped
17176 // mid-restore. The clauses are consumed by the same helper the FK
17177 // path has used since round 288 and recorded nowhere: SPG enforces
17178 // the constraint IMMEDIATELY either way, which fails earlier than
17179 // PG inside a transaction that violates-then-repairs — a refusal,
17180 // not a wrong answer. True deferral is the open remainder of F08.
17181 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17182 || (matches!(self.peek(), Token::Not)
17183 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17184 {
17185 // v7.39 (round 711) — CARRIED now (the storing half of
17186 // F08); round 621 only consumed.
17187 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17188 constraint_deferrable |= d;
17189 constraint_initially_deferred |= idef;
17190 continue;
17191 }
17192 if let Token::Ident(s) = self.peek()
17193 && s.eq_ignore_ascii_case("primary")
17194 {
17195 if is_primary_key {
17196 return Err(self.err("PRIMARY KEY specified twice".into()));
17197 }
17198 // Peek-ahead for the required `KEY` token.
17199 let next = self.tokens.get(self.pos + 1);
17200 let next_is_key = matches!(
17201 next,
17202 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17203 );
17204 if !next_is_key {
17205 return Err(self.err(format!(
17206 "expected KEY after PRIMARY in column def, got {:?}",
17207 next
17208 )));
17209 }
17210 self.advance(); // PRIMARY
17211 self.advance(); // KEY
17212 is_primary_key = true;
17213 if nullability_seen && nullable {
17214 return Err(self.err(
17215 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17216 ));
17217 }
17218 nullable = false;
17219 nullability_seen = true;
17220 continue;
17221 }
17222 // v7.13.0 — inline `UNIQUE` column constraint
17223 // (mailrs round-5 G2). Fold into a single-column
17224 // table-level UNIQUE at CREATE TABLE post-process time.
17225 if let Token::Ident(s) = self.peek()
17226 && s.eq_ignore_ascii_case("unique")
17227 {
17228 if is_unique {
17229 return Err(self.err("UNIQUE specified twice".into()));
17230 }
17231 self.advance();
17232 is_unique = true;
17233 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17234 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17235 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17236 let n1 = self.tokens.get(self.pos + 1);
17237 let n2 = self.tokens.get(self.pos + 2);
17238 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17239 self.advance(); // NULLS
17240 self.advance(); // NOT
17241 self.advance(); // DISTINCT
17242 unique_nulls_not_distinct = true;
17243 } else if matches!(n1, Some(Token::Distinct)) {
17244 self.advance(); // NULLS
17245 self.advance(); // DISTINCT
17246 }
17247 }
17248 continue;
17249 }
17250 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17251 // (mailrs round-5 G3). PG semantics: column-level
17252 // CHECK is equivalent to a table-level CHECK. Multiple
17253 // inline CHECKs on the same column AND together.
17254 if let Token::Ident(s) = self.peek()
17255 && s.eq_ignore_ascii_case("check")
17256 {
17257 self.advance();
17258 if !matches!(self.peek(), Token::LParen) {
17259 return Err(self.err(alloc::format!(
17260 "expected '(' after CHECK in column def, got {:?}",
17261 self.peek()
17262 )));
17263 }
17264 self.advance();
17265 let pred = self.parse_expr(0)?;
17266 if !matches!(self.peek(), Token::RParen) {
17267 return Err(self.err(alloc::format!(
17268 "expected ')' to close CHECK predicate, got {:?}",
17269 self.peek()
17270 )));
17271 }
17272 self.advance();
17273 check = Some(match check.take() {
17274 Some(prev) => Expr::Binary {
17275 op: BinOp::And,
17276 lhs: Box::new(prev),
17277 rhs: Box::new(pred),
17278 },
17279 None => pred,
17280 });
17281 continue;
17282 }
17283 break;
17284 }
17285 Ok(ColumnDef {
17286 name,
17287 ty,
17288 nullable,
17289 default,
17290 auto_increment,
17291 is_primary_key,
17292 is_unique,
17293 unique_nulls_not_distinct,
17294 constraint_deferrable,
17295 constraint_initially_deferred,
17296 check,
17297 user_type_ref,
17298 on_update_runtime,
17299 collation,
17300 collation_explicit,
17301 collation_name,
17302 is_unsigned,
17303 inline_enum_variants,
17304 inline_set_variants,
17305 generated_stored_expr,
17306 identity_always,
17307 mysql_int_width,
17308 mysql_fsp,
17309 })
17310 }
17311
17312 /// `NUMERIC` may appear without parameters, with one (precision
17313 /// only, scale=0), or with both. Returns `(precision, scale)` with
17314 /// 0 = unspecified for the bare form.
17315 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17316 if !matches!(self.peek(), Token::LParen) {
17317 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17318 // we surface it as precision=0 to mean "unconstrained" so
17319 // the engine doesn't need a separate variant.
17320 return Ok((0, 0));
17321 }
17322 self.advance();
17323 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17324 // it words the out-of-range case with the value it saw. SPG
17325 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17326 // accepts failed to parse at all; values wider than i128 are
17327 // carried by the arbitrary-precision form.
17328 let precision = match self.advance() {
17329 Token::Integer(n) if (1..=1000).contains(&n) => {
17330 u16::try_from(n).expect("range-checked")
17331 }
17332 Token::Integer(n) => {
17333 return Err(ParseError {
17334 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17335 token_pos: self.consumed_pos(),
17336 });
17337 }
17338 other => {
17339 return Err(ParseError {
17340 message: format!(
17341 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17342 ),
17343 token_pos: self.consumed_pos(),
17344 });
17345 }
17346 };
17347 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17348 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17349 // then overflows). A negative scale rounds to tens / hundreds / …
17350 let scale = if matches!(self.peek(), Token::Comma) {
17351 self.advance();
17352 let neg = if matches!(self.peek(), Token::Minus) {
17353 self.advance();
17354 true
17355 } else {
17356 false
17357 };
17358 match self.advance() {
17359 Token::Integer(n) => {
17360 let signed = if neg { -n } else { n };
17361 if !(-1000..=1000).contains(&signed) {
17362 return Err(ParseError {
17363 message: format!(
17364 "NUMERIC scale {signed} must be between -1000 and 1000"
17365 ),
17366 token_pos: self.consumed_pos(),
17367 });
17368 }
17369 i16::try_from(signed).expect("range-checked")
17370 }
17371 other => {
17372 return Err(ParseError {
17373 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17374 token_pos: self.consumed_pos(),
17375 });
17376 }
17377 }
17378 } else {
17379 0
17380 };
17381 if !matches!(self.peek(), Token::RParen) {
17382 return Err(self.err(format!(
17383 "expected ')' to close NUMERIC params, got {:?}",
17384 self.peek()
17385 )));
17386 }
17387 self.advance();
17388 Ok((precision, scale))
17389 }
17390
17391 /// Parse `(N)` where `N` is a positive integer literal — used by the
17392 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17393 /// for the error message.
17394 /// v6.0.1: parse the optional `USING <encoding>` clause that
17395 /// follows `VECTOR(N)` in a column definition. Missing clause
17396 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17397 /// ident → `ParseError` listing the encodings recognised today.
17398 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17399 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17400 return Ok(VecEncoding::F32);
17401 }
17402 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17403 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17404 // consume the token when the very next token is a known
17405 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17406 // `USING` for the caller — it's the rewrite-expression form.
17407 let n1 = self.tokens.get(self.pos + 1);
17408 let next_is_encoding = matches!(
17409 n1,
17410 Some(Token::Ident(s))
17411 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17412 );
17413 if !next_is_encoding {
17414 return Ok(VecEncoding::F32);
17415 }
17416 self.advance();
17417 let enc_ident = match self.advance() {
17418 Token::Ident(s) => s,
17419 other => {
17420 return Err(self.err(format!(
17421 "expected vector encoding after USING, got {other:?}"
17422 )));
17423 }
17424 };
17425 match enc_ident.to_ascii_lowercase().as_str() {
17426 "sq8" => Ok(VecEncoding::Sq8),
17427 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17428 // binary16 per-element storage.
17429 "half" => Ok(VecEncoding::F16),
17430 other => Err(self.err(format!(
17431 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17432 ))),
17433 }
17434 }
17435
17436 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17437 /// without consuming it. Returns `Some(N)` when the next
17438 /// tokens are `( <int> )`; None otherwise. Used by the
17439 /// TINYINT classifier to decide whether to map to Bool or
17440 /// SmallInt.
17441 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17442 if !matches!(self.peek(), Token::LParen) {
17443 return None;
17444 }
17445 let next = self.tokens.get(self.pos + 1)?;
17446 let n = match next {
17447 Token::Integer(n) => *n,
17448 _ => return None,
17449 };
17450 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17451 return None;
17452 }
17453 Some(n)
17454 }
17455
17456 /// v7.14.0 — consume an optional MySQL display-width
17457 /// parenthesised number after an integer type, returning
17458 /// nothing. `TINYINT(1)` etc.
17459 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17460 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17461 fn peek_paren_has_comma(&self) -> bool {
17462 let mut i = self.pos + 1;
17463 let mut depth = 1usize;
17464 while depth > 0 {
17465 match self.tokens.get(i) {
17466 Some(Token::LParen) => depth += 1,
17467 Some(Token::RParen) => depth -= 1,
17468 Some(Token::Comma) if depth == 1 => return true,
17469 None | Some(Token::Eof) => return false,
17470 _ => {}
17471 }
17472 i += 1;
17473 }
17474 false
17475 }
17476
17477 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17478 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17479 /// fractional-seconds precision that drives write truncation and render
17480 /// padding, where `consume_optional_paren_size` throws it away.
17481 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17482 fn take_optional_paren_size(&mut self) -> Option<u8> {
17483 let Some(Token::Integer(n)) = self
17484 .tokens
17485 .get(self.pos + 1)
17486 .filter(|_| matches!(self.peek(), Token::LParen))
17487 .cloned()
17488 else {
17489 self.consume_optional_paren_size();
17490 return None;
17491 };
17492 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17493 self.consume_optional_paren_size();
17494 return None;
17495 }
17496 self.consume_optional_paren_size();
17497 u8::try_from(n).ok()
17498 }
17499
17500 fn consume_optional_paren_size(&mut self) {
17501 if !matches!(self.peek(), Token::LParen) {
17502 return;
17503 }
17504 self.advance();
17505 // Skip until matching RParen (allow nested or any tokens).
17506 let mut depth = 1usize;
17507 while depth > 0 {
17508 match self.peek() {
17509 Token::LParen => depth += 1,
17510 Token::RParen => depth -= 1,
17511 Token::Eof => return,
17512 _ => {}
17513 }
17514 self.advance();
17515 }
17516 }
17517
17518 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17519 if !matches!(self.peek(), Token::LParen) {
17520 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17521 }
17522 self.advance();
17523 let n = match self.advance() {
17524 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17525 message: format!("{label} size too large: {n}"),
17526 token_pos: self.consumed_pos(),
17527 })?,
17528 other => {
17529 return Err(ParseError {
17530 message: format!("expected positive integer {label} size, got {other:?}"),
17531 token_pos: self.consumed_pos(),
17532 });
17533 }
17534 };
17535 if !matches!(self.peek(), Token::RParen) {
17536 return Err(self.err(format!(
17537 "expected ')' after {label} size, got {:?}",
17538 self.peek()
17539 )));
17540 }
17541 self.advance();
17542 Ok(n)
17543 }
17544
17545 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17546 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17547 /// key, like MySQL) whose action skips conflicting rows.
17548 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17549 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17550 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17551 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17552 /// common bulk-upsert spellings —
17553 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17554 /// REPLACE INTO t SELECT …
17555 /// — were a parse error / a duplicate-key failure respectively.
17556 ///
17557 /// Precedence: an explicitly written clause beats a statement-level flag.
17558 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17559 /// implicit `REPLACE` and `IGNORE` lowerings.
17560 fn parse_insert_conflict_clause(
17561 &mut self,
17562 replace: bool,
17563 ignore: bool,
17564 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17565 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17566 return Ok(Some(c));
17567 }
17568 if let Some(c) = self.parse_optional_on_conflict()? {
17569 return Ok(Some(c));
17570 }
17571 if replace {
17572 // REPLACE INTO = delete-then-insert, which PG spells as
17573 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17574 // reads an empty assignment list as "take the incoming row".
17575 return Ok(Some(crate::ast::OnConflictClause {
17576 target_columns: Vec::new(),
17577 index_where: None,
17578 constraint_name: None,
17579 mysql_lowered: true,
17580 action: crate::ast::OnConflictAction::Update {
17581 assignments: Vec::new(),
17582 where_: None,
17583 },
17584 }));
17585 }
17586 if ignore {
17587 return Ok(Some(Self::insert_ignore_clause()));
17588 }
17589 Ok(None)
17590 }
17591
17592 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17593 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17594 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17595 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17596 fn parse_optional_on_duplicate_key(
17597 &mut self,
17598 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17599 if !(matches!(self.peek(), Token::On)
17600 && matches!(self.tokens.get(self.pos + 1),
17601 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17602 {
17603 return Ok(None);
17604 }
17605 self.advance(); // ON
17606 self.advance(); // DUPLICATE
17607 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17608 return Err(self.err(format!(
17609 "expected KEY after ON DUPLICATE, got {:?}",
17610 self.peek()
17611 )));
17612 }
17613 self.advance();
17614 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17615 return Err(self.err(format!(
17616 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17617 self.peek()
17618 )));
17619 }
17620 self.advance();
17621 let mut assignments: Vec<(String, Expr)> = Vec::new();
17622 loop {
17623 let col = self.expect_ident_like()?;
17624 if !matches!(self.peek(), Token::Eq) {
17625 return Err(self.err(format!(
17626 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17627 self.peek()
17628 )));
17629 }
17630 self.advance();
17631 let mut expr = self.parse_expr(0)?;
17632 Self::rewrite_mysql_values_refs(&mut expr);
17633 assignments.push((col, expr));
17634 if matches!(self.peek(), Token::Comma) {
17635 self.advance();
17636 continue;
17637 }
17638 break;
17639 }
17640 Ok(Some(crate::ast::OnConflictClause {
17641 target_columns: Vec::new(),
17642 index_where: None,
17643 constraint_name: None,
17644 mysql_lowered: true,
17645 action: crate::ast::OnConflictAction::Update {
17646 assignments,
17647 where_: None,
17648 },
17649 }))
17650 }
17651
17652 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17653 crate::ast::OnConflictClause {
17654 target_columns: Vec::new(),
17655 index_where: None,
17656 constraint_name: None,
17657 mysql_lowered: true,
17658 action: crate::ast::OnConflictAction::Nothing,
17659 }
17660 }
17661
17662 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17663 debug_assert!(
17664 matches!(self.peek(), Token::Insert)
17665 || (replace
17666 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17667 );
17668 self.advance();
17669 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17670 // would raise a duplicate-key error instead of failing the statement,
17671 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17672 // plain ident to the lexer; only the MySQL dialect accepts it here.
17673 let ignore = self.mysql_dialect
17674 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17675 if ignore {
17676 self.advance();
17677 }
17678 if !matches!(self.peek(), Token::Into) {
17679 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17680 }
17681 self.advance();
17682 let table = self.expect_ident_like()?;
17683 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17684 // grammar requires the AS keyword here (a bare identifier would be
17685 // ambiguous with a column list). The alias is what the ON CONFLICT
17686 // DO UPDATE expressions refer to the target row by.
17687 let alias = if matches!(self.peek(), Token::As) {
17688 self.advance();
17689 Some(self.expect_ident_like()?)
17690 } else {
17691 None
17692 };
17693 // v7.39 (round 428) — MySQL's SET-form INSERT:
17694 // INSERT INTO t SET a = 1, b = 'x'
17695 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17696 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17697 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17698 // measured). So it lowers to the column list + one VALUES row and
17699 // rejoins the ordinary path, which already handles every one of
17700 // those. PG has no such spelling, hence the dialect gate.
17701 if self.mysql_dialect
17702 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17703 {
17704 self.advance(); // SET
17705 let mut names = Vec::new();
17706 let mut values = Vec::new();
17707 loop {
17708 names.push(self.expect_ident_like()?);
17709 if !matches!(self.peek(), Token::Eq) {
17710 return Err(self.err(alloc::format!(
17711 "expected '=' in INSERT … SET, got {:?}",
17712 self.peek()
17713 )));
17714 }
17715 self.advance();
17716 // `SET a = DEFAULT` rides the same `__column_default` marker
17717 // the VALUES-row and UPDATE-SET paths use; the INSERT
17718 // executor resolves it against the target column.
17719 if matches!(self.peek(), Token::Default) {
17720 self.advance();
17721 values.push(Expr::FunctionCall {
17722 name: "__column_default".to_string(),
17723 args: Vec::new(),
17724 });
17725 } else {
17726 values.push(self.parse_expr(0)?);
17727 }
17728 if matches!(self.peek(), Token::Comma) {
17729 self.advance();
17730 continue;
17731 }
17732 break;
17733 }
17734 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17735 let returning = self.parse_optional_returning()?;
17736 return Ok(Statement::Insert(InsertStatement {
17737 ctes: Vec::new(),
17738 table,
17739 alias,
17740 columns: Some(names),
17741 rows: alloc::vec![values],
17742 select_source: None,
17743 // MySQL's SET form has no `OVERRIDING …` clause (that is
17744 // PG's identity-column spelling).
17745 overriding: Overriding::None,
17746 mysql_ignore: ignore,
17747 on_conflict,
17748 returning,
17749 }));
17750 }
17751 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17752 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17753 // a parenthesized query source instead (PG select_with_parens:
17754 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17755 // both keywords are reserved in PG, so no column list can start
17756 // with them.
17757 let columns = if matches!(self.peek(), Token::LParen) {
17758 self.advance();
17759 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17760 let select_stmt = if self.peek_is_with_kw() {
17761 self.advance();
17762 self.parse_nested_with_select()?
17763 } else {
17764 match self.parse_select_stmt()? {
17765 Statement::Select(s) => s,
17766 other => {
17767 return Err(self.err(alloc::format!(
17768 "expected SELECT in parenthesized INSERT source, got {other:?}"
17769 )));
17770 }
17771 }
17772 };
17773 if !matches!(self.peek(), Token::RParen) {
17774 return Err(self.err(format!(
17775 "expected ')' after parenthesized INSERT source, got {:?}",
17776 self.peek()
17777 )));
17778 }
17779 self.advance();
17780 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17781 let returning = self.parse_optional_returning()?;
17782 return Ok(Statement::Insert(InsertStatement {
17783 ctes: Vec::new(),
17784 table,
17785 alias: alias.clone(),
17786 columns: None,
17787 rows: Vec::new(),
17788 select_source: Some(Box::new(select_stmt)),
17789 on_conflict,
17790 returning,
17791 overriding: Overriding::None,
17792 mysql_ignore: ignore,
17793 }));
17794 }
17795 let mut names = Vec::new();
17796 loop {
17797 names.push(self.expect_ident_like()?);
17798 match self.peek() {
17799 Token::Comma => {
17800 self.advance();
17801 }
17802 Token::RParen => {
17803 self.advance();
17804 break;
17805 }
17806 other => {
17807 return Err(self.err(format!(
17808 "expected ',' or ')' in INSERT column list, got {other:?}"
17809 )));
17810 }
17811 }
17812 }
17813 Some(names)
17814 } else {
17815 None
17816 };
17817 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17818 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17819 // is captured on the statement so the engine can apply PG's
17820 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17821 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17822 {
17823 self.advance();
17824 let which = self.expect_ident_like()?;
17825 let ov = if which.eq_ignore_ascii_case("system") {
17826 Overriding::System
17827 } else if which.eq_ignore_ascii_case("user") {
17828 Overriding::User
17829 } else {
17830 return Err(self.err(format!(
17831 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17832 )));
17833 };
17834 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17835 return Err(self.err(format!(
17836 "expected VALUE after OVERRIDING {}, got {:?}",
17837 which.to_ascii_uppercase(),
17838 self.peek()
17839 )));
17840 }
17841 self.advance();
17842 ov
17843 } else {
17844 Overriding::None
17845 };
17846 // `INSERT INTO t DEFAULT VALUES` — a single row made
17847 // entirely of column defaults. Lower to the permuted
17848 // column-list path with an empty list: every schema column
17849 // is unmapped, so the engine fills each from its default
17850 // (serials advance, plain defaults evaluate, the rest NULL).
17851 if matches!(self.peek(), Token::Default) {
17852 self.advance();
17853 if !matches!(self.peek(), Token::Values) {
17854 return Err(self.err(format!(
17855 "expected VALUES after DEFAULT in INSERT, got {:?}",
17856 self.peek()
17857 )));
17858 }
17859 self.advance();
17860 if columns.is_some() {
17861 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17862 }
17863 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17864 let returning = self.parse_optional_returning()?;
17865 return Ok(Statement::Insert(InsertStatement {
17866 ctes: Vec::new(),
17867 table,
17868 alias: alias.clone(),
17869 columns: Some(Vec::new()),
17870 rows: alloc::vec![Vec::new()],
17871 select_source: None,
17872 on_conflict,
17873 returning,
17874 overriding,
17875 mysql_ignore: ignore,
17876 }));
17877 }
17878 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17879 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17880 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17881 // SELECT …`) heads the SOURCE select, as in PG (the statement's
17882 // own WITH comes before INSERT).
17883 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17884 let select_stmt = if self.peek_is_with_kw() {
17885 self.advance();
17886 self.parse_nested_with_select()?
17887 } else {
17888 match self.parse_select_stmt()? {
17889 Statement::Select(s) => s,
17890 other => {
17891 return Err(self.err(alloc::format!(
17892 "expected SELECT after INSERT INTO ... target, got {other:?}"
17893 )));
17894 }
17895 }
17896 };
17897 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17898 let returning = self.parse_optional_returning()?;
17899 return Ok(Statement::Insert(InsertStatement {
17900 ctes: Vec::new(),
17901 table,
17902 alias: alias.clone(),
17903 columns,
17904 rows: Vec::new(),
17905 select_source: Some(Box::new(select_stmt)),
17906 on_conflict,
17907 returning,
17908 overriding,
17909 mysql_ignore: ignore,
17910 }));
17911 }
17912 if !matches!(self.peek(), Token::Values) {
17913 return Err(self.err(format!(
17914 "expected VALUES or SELECT after table name, got {:?}",
17915 self.peek()
17916 )));
17917 }
17918 self.advance();
17919 if !matches!(self.peek(), Token::LParen) {
17920 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17921 }
17922 let mut rows = Vec::new();
17923 loop {
17924 // Each iteration consumes one `(expr, expr, …)` tuple.
17925 if !matches!(self.peek(), Token::LParen) {
17926 return Err(self.err(format!(
17927 "expected '(' for next VALUES tuple, got {:?}",
17928 self.peek()
17929 )));
17930 }
17931 self.advance();
17932 let mut tuple = Vec::new();
17933 loop {
17934 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17935 // the column's declared default for that slot. Rides out as the
17936 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17937 // path uses; the INSERT executor resolves it per target column.
17938 if matches!(self.peek(), Token::Default) {
17939 self.advance();
17940 tuple.push(Expr::FunctionCall {
17941 name: "__column_default".to_string(),
17942 args: Vec::new(),
17943 });
17944 } else {
17945 tuple.push(self.parse_expr(0)?);
17946 }
17947 match self.peek() {
17948 Token::Comma => {
17949 self.advance();
17950 }
17951 Token::RParen => {
17952 self.advance();
17953 break;
17954 }
17955 other => {
17956 return Err(self.err(format!(
17957 "expected ',' or ')' in VALUES tuple, got {other:?}"
17958 )));
17959 }
17960 }
17961 }
17962 if tuple.is_empty() {
17963 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17964 }
17965 rows.push(tuple);
17966 // Continue with comma-separated tuples.
17967 if matches!(self.peek(), Token::Comma) {
17968 self.advance();
17969 } else {
17970 break;
17971 }
17972 }
17973 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17974 // to ON CONFLICT DO UPDATE with an empty conflict target
17975 // (the engine picks the table's first unique index, which
17976 // matches MySQL's any-unique-key behaviour for the common
17977 // single-key case). `VALUES(col)` in the assignments is
17978 // MySQL's spelling of EXCLUDED.col.
17979 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17980 let returning = self.parse_optional_returning()?;
17981 Ok(Statement::Insert(InsertStatement {
17982 ctes: Vec::new(),
17983 table,
17984 alias,
17985 columns,
17986 rows,
17987 select_source: None,
17988 on_conflict,
17989 returning,
17990 overriding,
17991 mysql_ignore: ignore,
17992 }))
17993 }
17994
17995 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
17996 /// the incoming row's value — exactly PG's EXCLUDED.col.
17997 fn rewrite_mysql_values_refs(e: &mut Expr) {
17998 match e {
17999 Expr::FunctionCall { name, args }
18000 if name.eq_ignore_ascii_case("values")
18001 && args.len() == 1
18002 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18003 {
18004 let Expr::Column(c) = &args[0] else {
18005 unreachable!("guarded above");
18006 };
18007 *e = Expr::Column(crate::ast::ColumnName {
18008 qualifier: Some("EXCLUDED".to_string()),
18009 name: c.name.clone(),
18010 });
18011 }
18012 Expr::FunctionCall { args, .. } => {
18013 for a in args {
18014 Self::rewrite_mysql_values_refs(a);
18015 }
18016 }
18017 Expr::Binary { lhs, rhs, .. } => {
18018 Self::rewrite_mysql_values_refs(lhs);
18019 Self::rewrite_mysql_values_refs(rhs);
18020 }
18021 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18022 Self::rewrite_mysql_values_refs(expr);
18023 }
18024 Expr::Case {
18025 operand,
18026 branches,
18027 else_branch,
18028 } => {
18029 if let Some(op) = operand {
18030 Self::rewrite_mysql_values_refs(op);
18031 }
18032 for (w, t) in branches {
18033 Self::rewrite_mysql_values_refs(w);
18034 Self::rewrite_mysql_values_refs(t);
18035 }
18036 if let Some(el) = else_branch {
18037 Self::rewrite_mysql_values_refs(el);
18038 }
18039 }
18040 _ => {}
18041 }
18042 }
18043
18044 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18045 /// clause sitting between the INSERT body and the trailing
18046 /// RETURNING. All keywords come in as bare idents; `ON` is
18047 /// a reserved Token though.
18048 fn parse_optional_on_conflict(
18049 &mut self,
18050 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18051 if !matches!(self.peek(), Token::On) {
18052 return Ok(None);
18053 }
18054 // Peek further: we want exactly "ON CONFLICT ...". If the
18055 // next ident isn't "conflict", let some other parser handle.
18056 let next_is_conflict = matches!(
18057 self.tokens.get(self.pos + 1),
18058 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18059 );
18060 if !next_is_conflict {
18061 return Ok(None);
18062 }
18063 self.advance(); // ON
18064 self.advance(); // CONFLICT
18065 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18066 // the constraint instead of listing columns (the pg_dump
18067 // form); the engine resolves it.
18068 let mut constraint_name: Option<String> = None;
18069 if matches!(self.peek(), Token::On) {
18070 self.advance(); // ON
18071 match self.advance() {
18072 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18073 }
18074 other => {
18075 return Err(self.err(alloc::format!(
18076 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18077 )));
18078 }
18079 }
18080 constraint_name = Some(self.expect_ident_like()?);
18081 }
18082 // Optional `(col [, col]*)` target list.
18083 let mut target_columns: Vec<String> = Vec::new();
18084 if matches!(self.peek(), Token::LParen) {
18085 self.advance();
18086 loop {
18087 target_columns.push(self.expect_ident_like()?);
18088 match self.peek() {
18089 Token::Comma => {
18090 self.advance();
18091 }
18092 Token::RParen => {
18093 self.advance();
18094 break;
18095 }
18096 other => {
18097 return Err(self.err(alloc::format!(
18098 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18099 )));
18100 }
18101 }
18102 }
18103 }
18104 // v7.39 (round 240) — optional index predicate after the target
18105 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18106 // PARTIAL unique index; SPG's arbiters are full indexes, which
18107 // satisfy any predicate, so it is parsed and carried but not
18108 // consulted (recorded residual: partial-unique-index arbiters).
18109 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18110 self.advance();
18111 Some(self.parse_expr(0)?)
18112 } else {
18113 None
18114 };
18115 // Required `DO`.
18116 match self.advance() {
18117 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18118 other => {
18119 return Err(self.err(alloc::format!(
18120 "expected DO after ON CONFLICT [(…)], got {other:?}"
18121 )));
18122 }
18123 }
18124 // Action: NOTHING | UPDATE SET …
18125 let action = match self.advance() {
18126 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18127 crate::ast::OnConflictAction::Nothing
18128 }
18129 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18130 self.parse_on_conflict_update_action()?
18131 }
18132 other => {
18133 return Err(self.err(alloc::format!(
18134 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18135 )));
18136 }
18137 };
18138 Ok(Some(crate::ast::OnConflictClause {
18139 target_columns,
18140 index_where,
18141 constraint_name,
18142 mysql_lowered: false,
18143 action,
18144 }))
18145 }
18146
18147 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18148 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18149 /// consumed `UPDATE`.
18150 fn parse_on_conflict_update_action(
18151 &mut self,
18152 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18153 // `SET`
18154 match self.advance() {
18155 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18156 other => {
18157 return Err(self.err(alloc::format!(
18158 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18159 )));
18160 }
18161 }
18162 let mut assignments: Vec<(String, Expr)> = Vec::new();
18163 loop {
18164 let col = self.expect_ident_like()?;
18165 if !matches!(self.peek(), Token::Eq) {
18166 return Err(self.err(alloc::format!(
18167 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18168 self.peek()
18169 )));
18170 }
18171 self.advance();
18172 let value = self.parse_expr(0)?;
18173 assignments.push((col, value));
18174 if matches!(self.peek(), Token::Comma) {
18175 self.advance();
18176 continue;
18177 }
18178 break;
18179 }
18180 let where_ = if matches!(self.peek(), Token::Where) {
18181 self.advance();
18182 Some(self.parse_expr(0)?)
18183 } else {
18184 None
18185 };
18186 Ok(crate::ast::OnConflictAction::Update {
18187 assignments,
18188 where_,
18189 })
18190 }
18191
18192 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18193 let mut items = Vec::new();
18194 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18195 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18196 // answers one zero-column row per row of t, and a bare `SELECT`
18197 // answers a single zero-column row. SPG required at least one
18198 // item, so both were syntax errors. Recognised by the token that
18199 // follows — nothing that can start an expression appears here.
18200 if self.select_list_is_empty_here() {
18201 return Ok(items);
18202 }
18203 loop {
18204 items.push(self.parse_select_item()?);
18205 if matches!(self.peek(), Token::Comma) {
18206 self.advance();
18207 } else {
18208 break;
18209 }
18210 }
18211 Ok(items)
18212 }
18213
18214 /// Is the target list empty at this point — i.e. does the next token
18215 /// end the SELECT's item list rather than start an item?
18216 fn select_list_is_empty_here(&self) -> bool {
18217 match self.peek() {
18218 Token::From
18219 | Token::Where
18220 | Token::Group
18221 | Token::Having
18222 | Token::Order
18223 | Token::Limit
18224 | Token::Offset
18225 | Token::Semicolon
18226 | Token::RParen
18227 | Token::Union
18228 | Token::Except
18229 | Token::Eof => true,
18230 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18231 // with unreserved keywords, so they arrive as plain idents.
18232 Token::Ident(s) => {
18233 s.eq_ignore_ascii_case("fetch")
18234 || s.eq_ignore_ascii_case("window")
18235 || s.eq_ignore_ascii_case("intersect")
18236 }
18237 _ => false,
18238 }
18239 }
18240
18241 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18242 if matches!(self.peek(), Token::Star) {
18243 self.advance();
18244 return Ok(SelectItem::Wildcard);
18245 }
18246 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18247 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18248 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18249 // `<ident> . *` with nothing binding tighter.
18250 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18251 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18252 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18253 {
18254 self.advance(); // qualifier
18255 self.advance(); // .
18256 self.advance(); // *
18257 return Ok(SelectItem::QualifiedWildcard(q));
18258 }
18259 }
18260 let start_tok = self.pos;
18261 let expr = self.parse_expr(0)?;
18262 let end_tok = self.consumed_pos();
18263 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18264 // multi-column function returns into columns. Marked here and lowered in
18265 // `parse_bare_select`, where the FROM clause is in hand.
18266 if matches!(self.peek(), Token::Dot)
18267 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18268 {
18269 self.advance(); // .
18270 self.advance(); // *
18271 return Ok(SelectItem::Expr {
18272 expr: Expr::FunctionCall {
18273 name: "__record_expand".to_string(),
18274 args: alloc::vec![expr],
18275 },
18276 alias: None,
18277 });
18278 }
18279 let alias = match self.parse_optional_alias()? {
18280 Some(a) => Some(a),
18281 None => self.mysql_item_label(&expr, start_tok, end_tok),
18282 };
18283 Ok(SelectItem::Expr { expr, alias })
18284 }
18285
18286 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18287 /// carries no `AS`, filled in here so every downstream path reports it
18288 /// without knowing the rule. `None` leaves the item un-aliased, which is
18289 /// what a PG session always gets.
18290 ///
18291 /// Measured against MariaDB 11, three rules and no more:
18292 ///
18293 /// | item | label | why |
18294 /// |------------------|------------|------------------------------|
18295 /// | `lbl.a` | `a` | a column reports its name |
18296 /// | `'it''s'` | `it's` | a string reports its VALUE |
18297 /// | `a + b` | `a + b` | anything else, source text |
18298 ///
18299 /// The third is why this lives in the parser at all: the label is the
18300 /// text the client WROTE, down to the spacing, so it cannot be printed
18301 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18302 ///
18303 /// Comments survive, and that is right: through a `mariadb` CLI both
18304 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18305 /// CLIENT stripping the comment before it sends. Asked over the raw
18306 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18307 /// produces.
18308 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18309 if !self.mysql_dialect {
18310 return None;
18311 }
18312 match expr {
18313 // A column already reports its own name downstream; naming it
18314 // again here would only re-state the qualifier the label drops.
18315 Expr::Column(_) => None,
18316 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18317 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18318 }
18319 }
18320
18321 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18322 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18323 /// with PG's default column1..columnN names; subsequent rows
18324 /// chain as UNION ALL peers. Shared by the FROM-position
18325 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18326 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18327 let mut row_selects: Vec<SelectStatement> = Vec::new();
18328 loop {
18329 if !matches!(self.peek(), Token::LParen) {
18330 return Err(self.err(alloc::format!(
18331 "expected '(' to start a VALUES row, got {:?}",
18332 self.peek()
18333 )));
18334 }
18335 self.advance(); // (
18336 let mut items: Vec<SelectItem> = Vec::new();
18337 loop {
18338 let expr = self.parse_expr(0)?;
18339 items.push(SelectItem::Expr {
18340 expr,
18341 alias: Some(alloc::format!("column{}", items.len() + 1)),
18342 });
18343 match self.peek() {
18344 Token::Comma => {
18345 self.advance();
18346 }
18347 Token::RParen => break,
18348 other => {
18349 return Err(self.err(alloc::format!(
18350 "expected ',' or ')' in VALUES row, got {other:?}"
18351 )));
18352 }
18353 }
18354 }
18355 self.advance(); // )
18356 row_selects.push(SelectStatement {
18357 locking: None,
18358 ctes: Vec::new(),
18359 distinct: false,
18360 distinct_on: Vec::new(),
18361 items,
18362 from: None,
18363 where_: None,
18364 group_by: None,
18365 group_by_all: false,
18366 having: None,
18367 unions: Vec::new(),
18368 order_by: Vec::new(),
18369 limit: None,
18370 offset: None,
18371 limit_with_ties: false,
18372 window_check_exprs: Vec::new(),
18373 });
18374 if matches!(self.peek(), Token::Comma) {
18375 self.advance();
18376 continue;
18377 }
18378 break;
18379 }
18380 let mut head = row_selects.remove(0);
18381 head.unions = row_selects
18382 .into_iter()
18383 .map(|s| (UnionKind::All, s))
18384 .collect();
18385 Ok(head)
18386 }
18387
18388 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18389 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18390 // children. It was read as a table NAMED `only`, so the query
18391 // failed on `relation "only" does not exist`.
18392 //
18393 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18394 // absorbed the keyword, reasoning that SPG's children are
18395 // separate relations a plain scan does not descend into, so ONLY
18396 // already described the scan. That stopped being true when a
18397 // partition parent started unioning its children: measured,
18398 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18399 // where PG answers 0. The flag is carried now.
18400 let mut only = false;
18401 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18402 && matches!(
18403 self.tokens.get(self.pos + 1),
18404 Some(Token::Ident(_) | Token::QuotedIdent(_))
18405 )
18406 {
18407 only = true;
18408 self.advance();
18409 }
18410 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18411 // for these SRFs the keyword is noise at parse time: the
18412 // join executor already substitutes outer-column references
18413 // into unnest_expr / generate_series_args per outer row
18414 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18415 // licences the correlation even without the keyword. Absorb
18416 // it and fall through to the SRF arms below.
18417 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18418 // just the four builtin SRFs: a user set-returning function on a JOIN's
18419 // right side is the whole point of LATERAL. The keyword stays noise at
18420 // parse time — the join executor substitutes the outer row into the
18421 // call's arguments per outer row.
18422 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18423 && matches!(
18424 self.tokens.get(self.pos + 1),
18425 // The json_each family has its OWN `LATERAL …` arm below, which
18426 // needs to see the keyword — absorbing it here would send those
18427 // calls down the generic table-function channel instead.
18428 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18429 )
18430 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18431 {
18432 self.advance(); // LATERAL
18433 }
18434 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18435 // set-returning function whose argument may reference a
18436 // preceding FROM item. We rewrite this to
18437 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18438 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18439 // executor handles per-outer-row evaluation and the
18440 // SRF-primary jsonb_each_text path handles the inner
18441 // materialisation. Sentori 0067 backfill is the dogfood
18442 // shape.
18443 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18444 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18445 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18446 {
18447 self.advance(); // LATERAL
18448 let each_fn = match self.peek() {
18449 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18450 _ => unreachable!(),
18451 };
18452 self.advance(); // jsonb_each[_text] / json_each[_text]
18453 self.advance(); // (
18454 let arg = self.parse_expr(0)?;
18455 if !matches!(self.peek(), Token::RParen) {
18456 return Err(self.err(alloc::format!(
18457 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18458 self.peek()
18459 )));
18460 }
18461 self.advance();
18462 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18463 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18464 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18465 // FROM jsonb_each_text(<arg>) AS __srf__
18466 // PG's `AS kv(key, value)` column-alias list maps
18467 // positions to names; default to (key, value) when
18468 // omitted (matching the SRF's natural column names).
18469 let srf_alias = "__srf__".to_string();
18470 let key_alias = column_aliases
18471 .first()
18472 .cloned()
18473 .unwrap_or_else(|| "key".to_string());
18474 let value_alias = column_aliases
18475 .get(1)
18476 .cloned()
18477 .unwrap_or_else(|| "value".to_string());
18478 let inner_select = crate::ast::SelectStatement {
18479 locking: None,
18480 ctes: Vec::new(),
18481 distinct: false,
18482 distinct_on: Vec::new(),
18483 items: alloc::vec![
18484 crate::ast::SelectItem::Expr {
18485 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18486 qualifier: Some(srf_alias.clone()),
18487 name: "key".to_string(),
18488 }),
18489 alias: Some(key_alias),
18490 },
18491 crate::ast::SelectItem::Expr {
18492 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18493 qualifier: Some(srf_alias.clone()),
18494 name: "value".to_string(),
18495 }),
18496 alias: Some(value_alias),
18497 },
18498 ],
18499 from: Some(crate::ast::FromClause {
18500 primary: TableRef {
18501 name: srf_alias.clone(),
18502 alias: Some(srf_alias.clone()),
18503 only: false,
18504 as_of_segment: None,
18505 unnest_expr: None,
18506 unnest_column_aliases: Vec::new(),
18507 with_ordinality: false,
18508 generate_series_args: None,
18509 lateral_subquery: None,
18510 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18511 table_fn_call: None,
18512 rows_from: None,
18513 json_table: None,
18514 scalar_fn_item: false,
18515 },
18516 joins: Vec::new(),
18517 }),
18518 where_: None,
18519 group_by: None,
18520 group_by_all: false,
18521 having: None,
18522 unions: Vec::new(),
18523 order_by: Vec::new(),
18524 limit: None,
18525 offset: None,
18526 limit_with_ties: false,
18527 window_check_exprs: Vec::new(),
18528 };
18529 return Ok(TableRef {
18530 name: alias.clone(),
18531 alias: Some(alias),
18532 only: false,
18533 as_of_segment: None,
18534 unnest_expr: None,
18535 unnest_column_aliases: Vec::new(),
18536 with_ordinality: false,
18537 generate_series_args: None,
18538 lateral_subquery: Some(Box::new(inner_select)),
18539 jsonb_each_text_arg: None,
18540 table_fn_call: None,
18541 rows_from: None,
18542 json_table: None,
18543 scalar_fn_item: false,
18544 });
18545 }
18546 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18547 // without an explicit `LATERAL` keyword is the same shape
18548 // PG accepts (SRF naturally licences lateral correlation).
18549 // We mirror the LATERAL rewrite when the argument syntactic-
18550 // ally references an outer column (Column { qualifier:
18551 // Some(_), … }). For simplicity we apply the rewrite
18552 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18553 // in the FROM-list — caller-side join parsing positions
18554 // this peek correctly.
18555 // (Implementation note: detection lives below; the LATERAL
18556 // branch above already covers the explicit form; the bare
18557 // form falls through to the plain SRF arm and the engine
18558 // treats it as a constant-arg SRF if no outer reference is
18559 // present.)
18560 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18561 // table. Detect at the head so it claims precedence over
18562 // every other table-ref shape (unnest / generate_series /
18563 // bare ident); the lateral subquery itself follows the
18564 // regular SELECT grammar.
18565 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18566 // t(cols)`. Each row lowers to a constant SELECT with PG's
18567 // default column1..columnN names; subsequent rows chain as
18568 // UNION ALL peers. The result rides the derived-table
18569 // lateral_subquery channel — zero executor work.
18570 if matches!(self.peek(), Token::LParen)
18571 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18572 {
18573 self.advance(); // (
18574 self.advance(); // VALUES
18575 let head = self.parse_values_rows_body()?;
18576 if !matches!(self.peek(), Token::RParen) {
18577 return Err(self.err(alloc::format!(
18578 "expected ')' after VALUES list, got {:?}",
18579 self.peek()
18580 )));
18581 }
18582 self.advance();
18583 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18584 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18585 return Ok(TableRef {
18586 name,
18587 alias: alias_ident,
18588 only: false,
18589 as_of_segment: None,
18590 unnest_expr: None,
18591 unnest_column_aliases: column_aliases,
18592 with_ordinality: false,
18593 generate_series_args: None,
18594 lateral_subquery: Some(Box::new(head)),
18595 jsonb_each_text_arg: None,
18596 table_fn_call: None,
18597 rows_from: None,
18598 json_table: None,
18599 scalar_fn_item: false,
18600 });
18601 }
18602 // v7.37.17 (17.6 siblings) — plain derived table:
18603 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18604 // lateral_subquery channel the explicit LATERAL form uses —
18605 // an uncorrelated inner SELECT executes identically. The
18606 // inner parse carries UNION tails (they live on
18607 // SelectStatement.unions).
18608 // v7.37 D.20 — the derived-table inner may itself be a
18609 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18610 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18611 // bare `(SELECT …)`. parse_one_statement already routes a leading
18612 // `(` set-op group (its LParen arm) and a leading WITH
18613 // (parse_with_cte_then_select), so widen the second-token gate to
18614 // Select | LParen | WITH.
18615 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18616 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18617 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18618 // has existed since the shorthand landed and `parse_bare_select`
18619 // already routes it ("valid anywhere a SELECT head is"); what was
18620 // missing is this second-token gate, and the CTE body's dispatch
18621 // below. Round 868 found both by putting the shorthand in a
18622 // subquery — the top-level forms had been the only ones tested.
18623 if matches!(self.peek(), Token::LParen)
18624 && (matches!(
18625 self.tokens.get(self.pos + 1),
18626 Some(Token::Select | Token::LParen | Token::Table)
18627 ) || matches!(self.tokens.get(self.pos + 1),
18628 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18629 {
18630 self.advance(); // (
18631 let inner = match self.parse_one_statement()? {
18632 Statement::Select(s) => s,
18633 other => {
18634 return Err(self.err(alloc::format!(
18635 "expected SELECT inside derived table ( … ), got {other:?}"
18636 )));
18637 }
18638 };
18639 if !matches!(self.peek(), Token::RParen) {
18640 return Err(self.err(alloc::format!(
18641 "expected ')' after derived-table subquery, got {:?}",
18642 self.peek()
18643 )));
18644 }
18645 self.advance();
18646 // `AS t(a, b)` column-alias list rides the
18647 // unnest_column_aliases field (same positional-rename
18648 // contract the unnest SRFs use).
18649 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18650 let name = alias_ident
18651 .clone()
18652 .unwrap_or_else(|| "subquery".to_string());
18653 return Ok(TableRef {
18654 name,
18655 alias: alias_ident,
18656 only: false,
18657 as_of_segment: None,
18658 unnest_expr: None,
18659 unnest_column_aliases: column_aliases,
18660 with_ordinality: false,
18661 generate_series_args: None,
18662 lateral_subquery: Some(Box::new(inner)),
18663 jsonb_each_text_arg: None,
18664 table_fn_call: None,
18665 rows_from: None,
18666 json_table: None,
18667 scalar_fn_item: false,
18668 });
18669 }
18670 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18671 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18672 {
18673 self.advance(); // LATERAL
18674 self.advance(); // (
18675 // Parse the inner SELECT.
18676 let inner = match self.parse_one_statement()? {
18677 Statement::Select(s) => s,
18678 other => {
18679 return Err(self.err(alloc::format!(
18680 "expected SELECT inside LATERAL ( … ), got {other:?}"
18681 )));
18682 }
18683 };
18684 if !matches!(self.peek(), Token::RParen) {
18685 return Err(self.err(alloc::format!(
18686 "expected ')' after LATERAL subquery, got {:?}",
18687 self.peek()
18688 )));
18689 }
18690 self.advance();
18691 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18692 // `(VALUES …) t(g)` derived table round-trips through view-body
18693 // Display, which renders on the lateral_subquery channel).
18694 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18695 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18696 return Ok(TableRef {
18697 name,
18698 alias: alias_ident,
18699 only: false,
18700 as_of_segment: None,
18701 unnest_expr: None,
18702 unnest_column_aliases: column_aliases,
18703 with_ordinality: false,
18704 generate_series_args: None,
18705 lateral_subquery: Some(Box::new(inner)),
18706 jsonb_each_text_arg: None,
18707 table_fn_call: None,
18708 rows_from: None,
18709 json_table: None,
18710 scalar_fn_item: false,
18711 });
18712 }
18713 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18714 // function as a FROM item. Emits one row per (key, value)
18715 // pair in the JSONB object argument as TEXT columns. May
18716 // be wrapped in CROSS JOIN LATERAL when the argument
18717 // references a preceding FROM item (sentori migration
18718 // 0067 backfill shape: `CROSS JOIN LATERAL
18719 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18720 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18721 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18722 {
18723 let each_fn = match self.peek() {
18724 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18725 _ => unreachable!(),
18726 };
18727 self.advance(); // jsonb_each[_text] / json_each[_text]
18728 self.advance(); // (
18729 let arg = self.parse_expr(0)?;
18730 if !matches!(self.peek(), Token::RParen) {
18731 return Err(self.err(alloc::format!(
18732 "expected ')' after {each_fn}() argument, got {:?}",
18733 self.peek()
18734 )));
18735 }
18736 self.advance();
18737 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18738 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18739 return Ok(TableRef {
18740 name,
18741 alias: alias_ident,
18742 only: false,
18743 as_of_segment: None,
18744 unnest_expr: None,
18745 // `AS t(k, v)` renames key/value positionally, same as the
18746 // LATERAL-position form already does.
18747 unnest_column_aliases: column_aliases,
18748 with_ordinality: false,
18749 generate_series_args: None,
18750 lateral_subquery: None,
18751 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18752 table_fn_call: None,
18753 rows_from: None,
18754 json_table: None,
18755 scalar_fn_item: false,
18756 });
18757 }
18758 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18759 // (+ json_ variants) — record-returning JSON functions with a
18760 // column-definition list. Desugar to a derived table that
18761 // projects each declared column from the JSON via `->>` + a cast,
18762 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18763 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18764 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18765 {
18766 return self.parse_json_to_record_from();
18767 }
18768 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18769 // row is a text[] of capture groups, so it cannot desugar to unnest
18770 // (that would flatten the array). Wrap it as a derived table
18771 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18772 // SRF path already emits one text[] row per match. PG names the column
18773 // `regexp_matches`; an `AS a(col)` alias overrides it.
18774 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18775 if s.eq_ignore_ascii_case("regexp_matches"))
18776 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18777 {
18778 self.advance(); // fn name
18779 self.advance(); // (
18780 let mut fn_args: Vec<Expr> = Vec::new();
18781 loop {
18782 fn_args.push(self.parse_expr(0)?);
18783 if matches!(self.peek(), Token::Comma) {
18784 self.advance();
18785 continue;
18786 }
18787 break;
18788 }
18789 if !matches!(self.peek(), Token::RParen) {
18790 return Err(self.err(alloc::format!(
18791 "expected ')' after regexp_matches() arguments, got {:?}",
18792 self.peek()
18793 )));
18794 }
18795 self.advance();
18796 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18797 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18798 // it, so it died on the `with` token while every other table function
18799 // accepted it.
18800 let with_ordinality = self.absorb_with_ordinality();
18801 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18802 let table_alias = alias_ident
18803 .clone()
18804 .unwrap_or_else(|| "regexp_matches".to_string());
18805 // PG names a single-column function's output column after the ALIAS
18806 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18807 // `m` reads as that column and not as a whole-row composite. Naming
18808 // it after the function regardless made `SELECT m[1] FROM … AS m`
18809 // subscript a record.
18810 let col_name = column_aliases
18811 .first()
18812 .cloned()
18813 .or_else(|| alias_ident.clone())
18814 .unwrap_or_else(|| "regexp_matches".to_string());
18815 let inner = crate::ast::SelectStatement {
18816 locking: None,
18817 ctes: Vec::new(),
18818 distinct: false,
18819 distinct_on: Vec::new(),
18820 items: alloc::vec![SelectItem::Expr {
18821 expr: Expr::FunctionCall {
18822 name: "regexp_matches".to_string(),
18823 args: fn_args,
18824 },
18825 alias: Some(col_name),
18826 }],
18827 from: None,
18828 where_: None,
18829 group_by: None,
18830 group_by_all: false,
18831 having: None,
18832 unions: Vec::new(),
18833 order_by: Vec::new(),
18834 limit: None,
18835 offset: None,
18836 limit_with_ties: false,
18837 window_check_exprs: Vec::new(),
18838 };
18839 return Ok(TableRef {
18840 name: table_alias.clone(),
18841 alias: Some(table_alias),
18842 only: false,
18843 as_of_segment: None,
18844 unnest_expr: None,
18845 unnest_column_aliases: column_aliases,
18846 with_ordinality,
18847 generate_series_args: None,
18848 lateral_subquery: Some(Box::new(inner)),
18849 jsonb_each_text_arg: None,
18850 table_fn_call: None,
18851 rows_from: None,
18852 json_table: None,
18853 // regexp_matches returns text[], a base type: `SELECT m FROM
18854 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18855 scalar_fn_item: true,
18856 });
18857 }
18858 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18859 // / json_ variants as a FROM item. Rewritten into
18860 // `unnest(<same fn>(<expr>))`: the scalar form returns the
18861 // elements as a TEXT array, and the existing unnest SRF path
18862 // materialises one row per element. PG's natural column name
18863 // is `value`; an `AS a(col)` column-alias list overrides it.
18864 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18865 if s.eq_ignore_ascii_case("jsonb_array_elements")
18866 || s.eq_ignore_ascii_case("json_array_elements")
18867 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18868 || s.eq_ignore_ascii_case("json_array_elements_text")
18869 || s.eq_ignore_ascii_case("jsonb_object_keys")
18870 || s.eq_ignore_ascii_case("json_object_keys")
18871 || s.eq_ignore_ascii_case("jsonb_path_query")
18872 || s.eq_ignore_ascii_case("json_path_query")
18873 || s.eq_ignore_ascii_case("generate_subscripts")
18874 || s.eq_ignore_ascii_case("string_to_table")
18875 || s.eq_ignore_ascii_case("regexp_split_to_table"))
18876 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18877 {
18878 let fn_name = match self.peek() {
18879 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18880 _ => unreachable!(),
18881 };
18882 self.advance(); // fn name
18883 self.advance(); // (
18884 let mut fn_args: Vec<Expr> = Vec::new();
18885 loop {
18886 fn_args.push(self.parse_expr(0)?);
18887 if matches!(self.peek(), Token::Comma) {
18888 self.advance();
18889 continue;
18890 }
18891 break;
18892 }
18893 if !matches!(self.peek(), Token::RParen) {
18894 return Err(self.err(alloc::format!(
18895 "expected ')' after {fn_name}() arguments, got {:?}",
18896 self.peek()
18897 )));
18898 }
18899 self.advance();
18900 let with_ordinality = self.absorb_with_ordinality();
18901 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18902 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18903 // PG's natural column name: the array-elements SRFs
18904 // declare an OUT parameter `value`; jsonb_object_keys
18905 // and generate_subscripts have none, so the column is
18906 // named after the function. A bare table alias on a
18907 // single-column SRF renames the column too (PG: `FROM
18908 // generate_subscripts(a, 1) AS s` projects column s) —
18909 // except for the OUT-parameter SRFs, whose column stays
18910 // `value` under a bare alias.
18911 let natural_col = if fn_name.ends_with("_array_elements")
18912 || fn_name.ends_with("_array_elements_text")
18913 {
18914 "value".to_string()
18915 } else {
18916 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18917 };
18918 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18919 // Keep any further entries — the second names the
18920 // ordinality column under WITH ORDINALITY.
18921 srf_cols.extend(column_aliases.into_iter().skip(1));
18922 // The *_to_table SRFs are row-streams over the existing
18923 // *_to_array scalars — map the call target; the display
18924 // name (alias / column defaults) keeps the SRF spelling.
18925 let call_name = match fn_name.as_str() {
18926 "string_to_table" => "string_to_array".to_string(),
18927 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18928 _ => fn_name,
18929 };
18930 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18931 // preceding FROM item (bare or qualified column) is correlated;
18932 // route it through the per-outer-row lateral channel.
18933 let expr = crate::ast::Expr::FunctionCall {
18934 name: call_name,
18935 args: fn_args,
18936 };
18937 let correlated = Self::expr_has_any_column(&expr);
18938 let tref = TableRef {
18939 name,
18940 alias: alias_ident,
18941 only: false,
18942 as_of_segment: None,
18943 unnest_expr: Some(Box::new(expr)),
18944 unnest_column_aliases: srf_cols,
18945 with_ordinality,
18946 generate_series_args: None,
18947 lateral_subquery: None,
18948 jsonb_each_text_arg: None,
18949 table_fn_call: None,
18950 rows_from: None,
18951 json_table: None,
18952 // Each of these returns a BASE type (jsonb / text / int), so the item's
18953 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18954 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18955 scalar_fn_item: !with_ordinality,
18956 };
18957 return Ok(if correlated {
18958 Self::wrap_correlated_srf(tref)
18959 } else {
18960 tref
18961 });
18962 }
18963 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18964 // explicit parallel-zip syntax. Each entry lowers to its
18965 // array-returning scalar form (unnest(x) → x itself; the
18966 // FROM-SRF rewrite family → their scalar array calls) and
18967 // the list rides the multi-arg unnest zip channel:
18968 // NULL-padded to the longest, WITH ORDINALITY appends the
18969 // counter. generate_series has no scalar array form and
18970 // errors honestly.
18971 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18972 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18973 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18974 {
18975 self.advance(); // ROWS
18976 self.advance(); // FROM
18977 self.advance(); // (
18978 let mut entries: Vec<Expr> = Vec::new();
18979 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18980 // Used only when some entry has no array form.
18981 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18982 loop {
18983 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18984 if !matches!(self.peek(), Token::LParen) {
18985 return Err(self.err(alloc::format!(
18986 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
18987 self.peek()
18988 )));
18989 }
18990 self.advance();
18991 let mut fn_args: Vec<Expr> = Vec::new();
18992 if !matches!(self.peek(), Token::RParen) {
18993 loop {
18994 fn_args.push(self.parse_expr(0)?);
18995 if matches!(self.peek(), Token::Comma) {
18996 self.advance();
18997 continue;
18998 }
18999 break;
19000 }
19001 }
19002 if !matches!(self.peek(), Token::RParen) {
19003 return Err(self.err(alloc::format!(
19004 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19005 self.peek()
19006 )));
19007 }
19008 self.advance();
19009 let entry = match fn_name.as_str() {
19010 "unnest" => {
19011 if fn_args.len() != 1 {
19012 return Err(
19013 self.err("unnest inside ROWS FROM takes exactly one array".into())
19014 );
19015 }
19016 fn_args.pop().expect("len checked")
19017 }
19018 "jsonb_array_elements"
19019 | "json_array_elements"
19020 | "jsonb_array_elements_text"
19021 | "json_array_elements_text"
19022 | "jsonb_object_keys"
19023 | "json_object_keys"
19024 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19025 name: fn_name,
19026 args: fn_args,
19027 },
19028 "string_to_table" => crate::ast::Expr::FunctionCall {
19029 name: "string_to_array".to_string(),
19030 args: fn_args,
19031 },
19032 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19033 name: "regexp_split_to_array".to_string(),
19034 args: fn_args,
19035 },
19036 // v7.39 (read01 round 74) — an SRF with no array form
19037 // (`generate_series`, a user `RETURNS SETOF` function) has no
19038 // scalar expression to zip, so the WHOLE list switches to the
19039 // rows_from channel, which runs each function and zips the
19040 // rows themselves. The all-array case keeps the old lowering:
19041 // it is well-trodden and this must not disturb it.
19042 _ => {
19043 generic.push((fn_name, fn_args));
19044 if matches!(self.peek(), Token::Comma) {
19045 self.advance();
19046 continue;
19047 }
19048 break;
19049 }
19050 };
19051 generic.push((
19052 // The array-able entries carry their lowered expr along, so a
19053 // MIXED list still works: the engine sees the scalar array
19054 // form and unnests it.
19055 "__array".to_string(),
19056 alloc::vec![entry.clone()],
19057 ));
19058 entries.push(entry);
19059 if matches!(self.peek(), Token::Comma) {
19060 self.advance();
19061 continue;
19062 }
19063 break;
19064 }
19065 if !matches!(self.peek(), Token::RParen) {
19066 return Err(self.err(alloc::format!(
19067 "expected ')' to close ROWS FROM, got {:?}",
19068 self.peek()
19069 )));
19070 }
19071 self.advance();
19072 let with_ordinality = self.absorb_with_ordinality();
19073 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19074 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19075 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19076 // list rides the generic channel.
19077 if generic.iter().any(|(n, _)| n != "__array") {
19078 let correlated = generic
19079 .iter()
19080 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19081 let tref = TableRef {
19082 name,
19083 alias: alias_ident,
19084 only: false,
19085 as_of_segment: None,
19086 unnest_expr: None,
19087 unnest_column_aliases,
19088 with_ordinality,
19089 generate_series_args: None,
19090 lateral_subquery: None,
19091 jsonb_each_text_arg: None,
19092 table_fn_call: None,
19093 rows_from: Some(generic),
19094 json_table: None,
19095 scalar_fn_item: false,
19096 };
19097 return Ok(if correlated {
19098 Self::wrap_correlated_srf(tref)
19099 } else {
19100 tref
19101 });
19102 }
19103 let correlated = entries.iter().any(Self::expr_has_any_column);
19104 let expr = if entries.len() == 1 {
19105 entries.pop().expect("len checked")
19106 } else {
19107 crate::ast::Expr::FunctionCall {
19108 name: "__unnest_zip".to_string(),
19109 args: entries,
19110 }
19111 };
19112 let tref = TableRef {
19113 name,
19114 alias: alias_ident,
19115 only: false,
19116 as_of_segment: None,
19117 unnest_expr: Some(Box::new(expr)),
19118 unnest_column_aliases,
19119 with_ordinality,
19120 generate_series_args: None,
19121 lateral_subquery: None,
19122 jsonb_each_text_arg: None,
19123 table_fn_call: None,
19124 rows_from: None,
19125 json_table: None,
19126 scalar_fn_item: false,
19127 };
19128 return Ok(if correlated {
19129 Self::wrap_correlated_srf(tref)
19130 } else {
19131 tref
19132 });
19133 }
19134 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19135 // source. Detect at the head before the bare-ident fallback;
19136 // unnest is not a reserved token.
19137 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19138 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19139 {
19140 self.advance(); // unnest
19141 self.advance(); // (
19142 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19143 while matches!(self.peek(), Token::Comma) {
19144 self.advance();
19145 srf_args.push(self.parse_expr(0)?);
19146 }
19147 if !matches!(self.peek(), Token::RParen) {
19148 return Err(self.err(alloc::format!(
19149 "expected ')' after unnest() argument, got {:?}",
19150 self.peek()
19151 )));
19152 }
19153 self.advance();
19154 // Multi-arg unnest(a, b, …) zips the arrays in
19155 // parallel, NULL-padding to the longest (PG's ROWS
19156 // FROM shorthand). Lower onto the unnest channel as an
19157 // internal marker call the executors unpack.
19158 let expr = if srf_args.len() == 1 {
19159 srf_args.pop().expect("len checked")
19160 } else {
19161 crate::ast::Expr::FunctionCall {
19162 name: "__unnest_zip".to_string(),
19163 args: srf_args,
19164 }
19165 };
19166 let with_ordinality = self.absorb_with_ordinality();
19167 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19168 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19169 let correlated = Self::expr_has_any_column(&expr);
19170 let tref = TableRef {
19171 name,
19172 alias: alias_ident,
19173 only: false,
19174 as_of_segment: None,
19175 unnest_expr: Some(Box::new(expr)),
19176 unnest_column_aliases,
19177 with_ordinality,
19178 generate_series_args: None,
19179 lateral_subquery: None,
19180 jsonb_each_text_arg: None,
19181 table_fn_call: None,
19182 rows_from: None,
19183 json_table: None,
19184 scalar_fn_item: false,
19185 };
19186 return Ok(if correlated {
19187 Self::wrap_correlated_srf(tref)
19188 } else {
19189 tref
19190 });
19191 }
19192 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19193 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19194 // generic table-fn arg parser can't read), so it is intercepted
19195 // here BEFORE the generic dispatch. The doc expr may reference
19196 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19197 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19198 if s.eq_ignore_ascii_case("json_table"))
19199 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19200 {
19201 let tref = self.parse_json_table_ref()?;
19202 let correlated = tref
19203 .json_table
19204 .as_deref()
19205 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19206 return Ok(if correlated {
19207 Self::wrap_correlated_srf(tref)
19208 } else {
19209 tref
19210 });
19211 }
19212 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19213 // functions dispatched by name (`pg_partition_tree('t')`,
19214 // `pg_partition_ancestors('t')`). Same head-detection shape as
19215 // unnest; the engine executor owns the row shape per function.
19216 // v7.39 (read01 round 65) — and a USER function in FROM position
19217 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19218 // (generate_series / unnest / the json_each family) keep it — their arms
19219 // sit further down, so they are excluded here by name rather than by
19220 // ordering. Anything else that is an ident followed by `(` is a table
19221 // function; the engine executor decides whether it is a builtin, a
19222 // set-returning user function, or an error.
19223 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19224 if !s.eq_ignore_ascii_case("generate_series")
19225 && !s.eq_ignore_ascii_case("unnest")
19226 && !is_json_each_name(s))
19227 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19228 {
19229 // Body out-of-line — this parse sits on the FROM/subquery
19230 // recursion chain (debug frame-cliff discipline).
19231 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19232 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19233 // outer row, so it rides the lateral channel. Same rule the unnest
19234 // arm uses.
19235 let tref = self.parse_table_fn_ref()?;
19236 let correlated = tref
19237 .table_fn_call
19238 .as_deref()
19239 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19240 return Ok(if correlated {
19241 Self::wrap_correlated_srf(tref)
19242 } else {
19243 tref
19244 });
19245 }
19246 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19247 // [, step])` set-returning source. Same shape as unnest:
19248 // detect at the head, parse the comma-separated arg list,
19249 // dispatch downstream through the engine's set-returning
19250 // path. Supports integer triplets (mailrs's `WITH row_no AS
19251 // (SELECT * FROM generate_series(1, N))` pattern) and
19252 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19253 // date-range iteration pattern, which pre-3.10 had no
19254 // direct equivalent in SPG).
19255 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19256 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19257 {
19258 self.advance(); // generate_series
19259 self.advance(); // (
19260 let mut args: Vec<Expr> = Vec::new();
19261 loop {
19262 args.push(self.parse_expr(0)?);
19263 if matches!(self.peek(), Token::Comma) {
19264 self.advance();
19265 continue;
19266 }
19267 break;
19268 }
19269 if !matches!(self.peek(), Token::RParen) {
19270 return Err(self.err(alloc::format!(
19271 "expected ')' after generate_series() arguments, got {:?}",
19272 self.peek()
19273 )));
19274 }
19275 self.advance();
19276 if args.len() < 2 || args.len() > 3 {
19277 return Err(self.err(alloc::format!(
19278 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19279 args.len()
19280 )));
19281 }
19282 let with_ordinality = self.absorb_with_ordinality();
19283 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19284 let name = alias_ident
19285 .clone()
19286 .unwrap_or_else(|| "generate_series".to_string());
19287 let correlated = args.iter().any(Self::expr_has_any_column);
19288 let tref = TableRef {
19289 name,
19290 alias: alias_ident,
19291 only: false,
19292 as_of_segment: None,
19293 unnest_expr: None,
19294 unnest_column_aliases: column_aliases,
19295 with_ordinality,
19296 generate_series_args: Some(args),
19297 lateral_subquery: None,
19298 jsonb_each_text_arg: None,
19299 table_fn_call: None,
19300 rows_from: None,
19301 json_table: None,
19302 scalar_fn_item: false,
19303 };
19304 return Ok(if correlated {
19305 Self::wrap_correlated_srf(tref)
19306 } else {
19307 tref
19308 });
19309 }
19310 // v7.16.2 — preserve information_schema / pg_catalog
19311 // qualifiers (mailrs round-10 A.3). The generic
19312 // `expect_ident_like` strip silently drops the schema;
19313 // we want the engine to recognise these PG meta tables
19314 // and synthesise rows from the live catalog. Produce a
19315 // synthetic name (`__spg_info_columns` etc.) so the
19316 // engine's SELECT-side router can dispatch without
19317 // clashing with any user-defined `columns` table.
19318 let name = if let Some(synth) = self.try_peek_meta_qualified() {
19319 synth
19320 } else if let Some(synth) = self.try_peek_meta_bare() {
19321 synth
19322 } else {
19323 self.expect_ident_like()?
19324 };
19325 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19326 // time-travel clause. Parse BEFORE the alias so the
19327 // alias can still ride at the tail (`tbl AS OF SEGMENT
19328 // '5' alias`). `AS` is a reserved keyword token, while
19329 // `OF` and `SEGMENT` are bare idents.
19330 let as_of_segment = if matches!(self.peek(), Token::As)
19331 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19332 {
19333 self.advance(); // AS
19334 self.advance(); // OF
19335 let kw = match self.peek().clone() {
19336 Token::Ident(s) | Token::QuotedIdent(s) => s,
19337 other => {
19338 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19339 }
19340 };
19341 if !kw.eq_ignore_ascii_case("segment") {
19342 return Err(self.err(format!(
19343 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19344 )));
19345 }
19346 self.advance();
19347 // Segment id literal — accept either a string or
19348 // integer for operator ergonomics.
19349 let id = match self.advance() {
19350 Token::String(s) => s
19351 .parse::<u32>()
19352 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19353 Token::Integer(n) => u32::try_from(n)
19354 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19355 other => {
19356 return Err(self.err(format!(
19357 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19358 )));
19359 }
19360 };
19361 Some(id)
19362 } else {
19363 None
19364 };
19365 // TABLESAMPLE is not a reserved token — keep the bare-ident
19366 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19367 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19368 {
19369 None
19370 } else {
19371 self.parse_optional_alias()?
19372 };
19373 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19374 // (PG grammar). BERNOULLI lowers to a per-row
19375 // `random() < p/100` conjunct on the enclosing SELECT's
19376 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19377 // shares the lowering: SPG has no page structure to
19378 // sample, and the row-level form returns the same expected
19379 // fraction. REPEATABLE(seed) promises a deterministic
19380 // sample SPG cannot honour yet — honest error rather than
19381 // a silently ignored seed.
19382 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19383 self.advance();
19384 let method = self.expect_ident_like()?;
19385 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19386 return Err(self.err(alloc::format!(
19387 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19388 )));
19389 }
19390 if !matches!(self.peek(), Token::LParen) {
19391 return Err(self.err(alloc::format!(
19392 "expected '(' after TABLESAMPLE {}, got {:?}",
19393 method.to_ascii_uppercase(),
19394 self.peek()
19395 )));
19396 }
19397 self.advance();
19398 let percent = self.parse_expr(0)?;
19399 if !matches!(self.peek(), Token::RParen) {
19400 return Err(self.err(alloc::format!(
19401 "expected ')' after TABLESAMPLE percentage, got {:?}",
19402 self.peek()
19403 )));
19404 }
19405 self.advance();
19406 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19407 // `seed`, so the sample is stable across repeats and rescans.
19408 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19409 let mut sample_seed: Option<Expr> = None;
19410 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19411 self.advance();
19412 if !matches!(self.peek(), Token::LParen) {
19413 return Err(self.err(alloc::format!(
19414 "expected '(' after REPEATABLE, got {:?}",
19415 self.peek()
19416 )));
19417 }
19418 self.advance();
19419 let seed = self.parse_expr(0)?;
19420 if !matches!(self.peek(), Token::RParen) {
19421 return Err(self.err(alloc::format!(
19422 "expected ')' after REPEATABLE seed, got {:?}",
19423 self.peek()
19424 )));
19425 }
19426 self.advance();
19427 sample_seed = Some(seed);
19428 }
19429 let draw = match sample_seed {
19430 Some(seed) => Expr::FunctionCall {
19431 name: "__tsm_fract".to_string(),
19432 args: alloc::vec![seed],
19433 },
19434 None => Expr::FunctionCall {
19435 name: "random".to_string(),
19436 args: Vec::new(),
19437 },
19438 };
19439 self.pending_sample_preds.push(Expr::Binary {
19440 lhs: Box::new(draw),
19441 op: crate::ast::BinOp::Lt,
19442 rhs: Box::new(Expr::Binary {
19443 lhs: Box::new(percent),
19444 op: crate::ast::BinOp::Div,
19445 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19446 }),
19447 });
19448 }
19449 Ok(TableRef {
19450 name,
19451 alias,
19452 only,
19453 as_of_segment,
19454 unnest_expr: None,
19455 unnest_column_aliases: Vec::new(),
19456 with_ordinality: false,
19457 generate_series_args: None,
19458 lateral_subquery: None,
19459 jsonb_each_text_arg: None,
19460 table_fn_call: None,
19461 rows_from: None,
19462 json_table: None,
19463 scalar_fn_item: false,
19464 })
19465 }
19466
19467 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19468 /// but also accepts `AS alias(col [, col, …])` — the
19469 /// PG-standard table-function column-list form. The column
19470 /// list is only honoured when paired with `UNNEST(...)` in
19471 /// the parent; other call sites currently discard it.
19472 /// True when the expression tree contains a qualified column
19473 /// reference (`t.col`) — the syntactic marker that an SRF
19474 /// argument correlates with a preceding FROM item.
19475 fn expr_has_qualified_column(e: &Expr) -> bool {
19476 match e {
19477 Expr::Column(c) => c.qualifier.is_some(),
19478 Expr::Binary { lhs, rhs, .. } => {
19479 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19480 }
19481 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19482 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19483 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19484 Expr::Case {
19485 operand,
19486 branches,
19487 else_branch,
19488 } => {
19489 operand
19490 .as_deref()
19491 .is_some_and(Self::expr_has_qualified_column)
19492 || branches.iter().any(|(w, t)| {
19493 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19494 })
19495 || else_branch
19496 .as_deref()
19497 .is_some_and(Self::expr_has_qualified_column)
19498 }
19499 _ => false,
19500 }
19501 }
19502
19503 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19504 /// counts a bare (unqualified) column. A set-returning function has no
19505 /// input columns of its own, so ANY column in its arguments is an outer
19506 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19507 fn expr_has_any_column(e: &Expr) -> bool {
19508 match e {
19509 Expr::Column(_) => true,
19510 Expr::Binary { lhs, rhs, .. } => {
19511 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19512 }
19513 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19514 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19515 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19516 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19517 // constructor or subscript fell to the `_ => false` arm, so
19518 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19519 // channel and the eager peer eval answered `column "x" does
19520 // not exist` (the substitution walker already recurses both
19521 // shapes; only this detector was blind to them).
19522 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19523 Expr::ArraySubscript { target, index } => {
19524 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19525 }
19526 Expr::Case {
19527 operand,
19528 branches,
19529 else_branch,
19530 } => {
19531 operand.as_deref().is_some_and(Self::expr_has_any_column)
19532 || branches
19533 .iter()
19534 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19535 || else_branch
19536 .as_deref()
19537 .is_some_and(Self::expr_has_any_column)
19538 }
19539 _ => false,
19540 }
19541 }
19542
19543 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19544 /// `generate_series(1, t.n)`) into the lateral_subquery
19545 /// channel: `SELECT * FROM <srf>` executes per outer row with
19546 /// outer references substituted (v7.37.43-T4.5 machinery).
19547 /// Uncorrelated SRFs stay on their plain channels.
19548 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19549 let name = srf.name.clone();
19550 let alias = srf.alias.clone();
19551 let inner = crate::ast::SelectStatement {
19552 locking: None,
19553 ctes: Vec::new(),
19554 distinct: false,
19555 distinct_on: Vec::new(),
19556 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19557 from: Some(crate::ast::FromClause {
19558 primary: srf,
19559 joins: Vec::new(),
19560 }),
19561 where_: None,
19562 group_by: None,
19563 group_by_all: false,
19564 having: None,
19565 unions: Vec::new(),
19566 order_by: Vec::new(),
19567 limit: None,
19568 offset: None,
19569 limit_with_ties: false,
19570 window_check_exprs: Vec::new(),
19571 };
19572 TableRef {
19573 name,
19574 alias,
19575 only: false,
19576 as_of_segment: None,
19577 unnest_expr: None,
19578 unnest_column_aliases: Vec::new(),
19579 with_ordinality: false,
19580 generate_series_args: None,
19581 lateral_subquery: Some(Box::new(inner)),
19582 jsonb_each_text_arg: None,
19583 table_fn_call: None,
19584 rows_from: None,
19585 json_table: None,
19586 scalar_fn_item: false,
19587 }
19588 }
19589
19590 /// True when the expression tree contains an unresolved
19591 /// `OVER w` marker (see parse_over_clause).
19592 fn expr_has_named_window(e: &Expr) -> bool {
19593 match e {
19594 Expr::WindowFunction { partition_by, .. } => matches!(
19595 partition_by.as_slice(),
19596 [Expr::Column(c)] if matches!(
19597 c.qualifier.as_deref(),
19598 Some("__named_window__") | Some("__named_window_ref__")
19599 )
19600 ),
19601 Expr::Binary { lhs, rhs, .. } => {
19602 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19603 }
19604 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19605 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19606 Expr::Case {
19607 operand,
19608 branches,
19609 else_branch,
19610 } => {
19611 operand.as_deref().is_some_and(Self::expr_has_named_window)
19612 || branches.iter().any(|(w, t)| {
19613 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19614 })
19615 || else_branch
19616 .as_deref()
19617 .is_some_and(Self::expr_has_named_window)
19618 }
19619 _ => false,
19620 }
19621 }
19622
19623 /// v7.39 (round 705) — the NAMES the expression references through the
19624 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19625 /// definitions nothing referenced. Traversal mirrors
19626 /// `expr_has_named_window` above.
19627 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19628 match e {
19629 Expr::WindowFunction { partition_by, .. } => {
19630 if let [Expr::Column(c)] = partition_by.as_slice()
19631 && matches!(
19632 c.qualifier.as_deref(),
19633 Some("__named_window__") | Some("__named_window_ref__")
19634 )
19635 {
19636 into.push(c.name.clone());
19637 }
19638 }
19639 Expr::Binary { lhs, rhs, .. } => {
19640 Self::collect_named_window_refs(lhs, into);
19641 Self::collect_named_window_refs(rhs, into);
19642 }
19643 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19644 Self::collect_named_window_refs(expr, into);
19645 }
19646 Expr::FunctionCall { args, .. } => {
19647 for a in args {
19648 Self::collect_named_window_refs(a, into);
19649 }
19650 }
19651 Expr::Case {
19652 operand,
19653 branches,
19654 else_branch,
19655 } => {
19656 if let Some(o) = operand.as_deref() {
19657 Self::collect_named_window_refs(o, into);
19658 }
19659 for (w, t) in branches {
19660 Self::collect_named_window_refs(w, into);
19661 Self::collect_named_window_refs(t, into);
19662 }
19663 if let Some(eb) = else_branch.as_deref() {
19664 Self::collect_named_window_refs(eb, into);
19665 }
19666 }
19667 _ => {}
19668 }
19669 }
19670
19671 /// Inline named-window definitions into the `OVER w` markers.
19672 /// An unknown name errors (PG: window "w" does not exist).
19673 #[allow(clippy::type_complexity)]
19674 fn substitute_named_windows(
19675 e: &mut Expr,
19676 defs: &[(
19677 String,
19678 (
19679 Vec<Expr>,
19680 Vec<(Expr, bool, Option<bool>)>,
19681 Option<WindowFrame>,
19682 ),
19683 )],
19684 ) -> Result<(), String> {
19685 match e {
19686 Expr::WindowFunction {
19687 partition_by,
19688 order_by,
19689 frame,
19690 ..
19691 } => {
19692 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19693 // from the bare `OVER w1` (a plain reference).
19694 let named = match partition_by.as_slice() {
19695 [Expr::Column(c)] => match c.qualifier.as_deref() {
19696 Some("__named_window__") => Some((c.name.clone(), false)),
19697 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19698 _ => None,
19699 },
19700 _ => None,
19701 };
19702 if let Some((wname, is_copy)) = named {
19703 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19704 else {
19705 return Err(alloc::format!("window {wname:?} does not exist"));
19706 };
19707 if !is_copy {
19708 *partition_by = def.0.clone();
19709 *order_by = def.1.clone();
19710 *frame = def.2.clone();
19711 return Ok(());
19712 }
19713 // v7.39 (round 229) — PG's copy rules, probed against
19714 // 18.4: a copy inherits the partitioning, may supply an
19715 // ordering only when the base has none, and may not copy
19716 // a base that already carries a frame (its own frame
19717 // would be ambiguous with the inherited one).
19718 if !def.1.is_empty() && !order_by.is_empty() {
19719 return Err(alloc::format!(
19720 "cannot override ORDER BY clause of window \"{wname}\""
19721 ));
19722 }
19723 if def.2.is_some() {
19724 return Err(alloc::format!(
19725 "cannot copy window \"{wname}\" because it has a frame clause"
19726 ));
19727 }
19728 *partition_by = def.0.clone();
19729 if order_by.is_empty() {
19730 *order_by = def.1.clone();
19731 }
19732 }
19733 Ok(())
19734 }
19735 Expr::Binary { lhs, rhs, .. } => {
19736 Self::substitute_named_windows(lhs, defs)?;
19737 Self::substitute_named_windows(rhs, defs)
19738 }
19739 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19740 Self::substitute_named_windows(expr, defs)
19741 }
19742 Expr::FunctionCall { args, .. } => {
19743 for a in args {
19744 Self::substitute_named_windows(a, defs)?;
19745 }
19746 Ok(())
19747 }
19748 Expr::Case {
19749 operand,
19750 branches,
19751 else_branch,
19752 } => {
19753 if let Some(op) = operand {
19754 Self::substitute_named_windows(op, defs)?;
19755 }
19756 for (w, t) in branches {
19757 Self::substitute_named_windows(w, defs)?;
19758 Self::substitute_named_windows(t, defs)?;
19759 }
19760 if let Some(el) = else_branch {
19761 Self::substitute_named_windows(el, defs)?;
19762 }
19763 Ok(())
19764 }
19765 _ => Ok(()),
19766 }
19767 }
19768
19769 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19770 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19771 /// composition.
19772 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19773 debug_assert!(matches!(self.peek(), Token::Table));
19774 self.advance(); // TABLE
19775 let tname = self.expect_ident_like()?;
19776 Ok(SelectStatement {
19777 locking: None,
19778 ctes: Vec::new(),
19779 distinct: false,
19780 distinct_on: Vec::new(),
19781 items: alloc::vec![SelectItem::Wildcard],
19782 from: Some(FromClause {
19783 primary: TableRef {
19784 name: tname,
19785 alias: None,
19786 only: false,
19787 as_of_segment: None,
19788 unnest_expr: None,
19789 unnest_column_aliases: Vec::new(),
19790 with_ordinality: false,
19791 generate_series_args: None,
19792 lateral_subquery: None,
19793 jsonb_each_text_arg: None,
19794 table_fn_call: None,
19795 rows_from: None,
19796 json_table: None,
19797 scalar_fn_item: false,
19798 },
19799 joins: Vec::new(),
19800 }),
19801 where_: None,
19802 group_by: None,
19803 group_by_all: false,
19804 having: None,
19805 unions: Vec::new(),
19806 order_by: Vec::new(),
19807 limit: None,
19808 offset: None,
19809 limit_with_ties: false,
19810 window_check_exprs: Vec::new(),
19811 })
19812 }
19813
19814 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19815 /// variants) → a derived table that reads each declared column out of
19816 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19817 /// `jsonb_array_elements(J)` (one row per element, column `value`);
19818 /// the scalar *record form projects a single row straight off `J`.
19819 /// Rides the existing lateral-subquery channel, so no new executor or
19820 /// AST is needed.
19821 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19822 use crate::ast::{
19823 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19824 };
19825 let fn_name = match self.peek() {
19826 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19827 _ => unreachable!("caller guarded is_json_to_record_name"),
19828 };
19829 self.advance(); // fn name
19830 self.advance(); // (
19831 let mut arg = self.parse_expr(0)?;
19832 // populate_record(base, json): the base only carries the record
19833 // type here — the JSON argument is the second expression.
19834 let mut base: Option<Expr> = None;
19835 if matches!(self.peek(), Token::Comma) {
19836 self.advance();
19837 base = Some(arg);
19838 arg = self.parse_expr(0)?;
19839 }
19840 if !matches!(self.peek(), Token::RParen) {
19841 return Err(self.err(alloc::format!(
19842 "expected ')' after {fn_name}() argument, got {:?}",
19843 self.peek()
19844 )));
19845 }
19846 self.advance(); // )
19847 let is_set = fn_name.ends_with("recordset");
19848 // `[AS] alias ( col type [, …] )` column-definition list.
19849 if matches!(self.peek(), Token::As) {
19850 self.advance();
19851 }
19852 let alias_opt = match self.peek() {
19853 Token::Ident(s) | Token::QuotedIdent(s) => {
19854 let a = s.clone();
19855 self.advance();
19856 Some(a)
19857 }
19858 _ => None,
19859 };
19860 // v7.39 (read01 round 76) — the populate family's canonical PG
19861 // spelling carries no column list at all: the row shape comes from
19862 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19863 // j)`). The parser has no catalog, so hand the two arguments to the
19864 // engine's table-function channel, which does. Only `*_to_record*`
19865 // (whose base is bare `record`) genuinely requires the list.
19866 if !matches!(self.peek(), Token::LParen) {
19867 if let Some(base_expr) = base {
19868 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19869 return Ok(TableRef {
19870 name: alias.clone(),
19871 alias: Some(alias),
19872 only: false,
19873 as_of_segment: None,
19874 unnest_expr: None,
19875 unnest_column_aliases: Vec::new(),
19876 with_ordinality: false,
19877 generate_series_args: None,
19878 lateral_subquery: None,
19879 jsonb_each_text_arg: None,
19880 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19881 rows_from: None,
19882 json_table: None,
19883 scalar_fn_item: false,
19884 });
19885 }
19886 return Err(self.err(alloc::format!(
19887 "expected '(' to start the {fn_name} column-definition list, got {:?}",
19888 self.peek()
19889 )));
19890 }
19891 let Some(alias) = alias_opt else {
19892 return Err(self.err(alloc::format!(
19893 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19894 )));
19895 };
19896 self.advance(); // (
19897 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19898 loop {
19899 let col = self.expect_ident_like()?;
19900 let ty = self.parse_cast_target()?;
19901 coldefs.push((col, ty));
19902 if matches!(self.peek(), Token::Comma) {
19903 self.advance();
19904 continue;
19905 }
19906 if matches!(self.peek(), Token::RParen) {
19907 self.advance();
19908 break;
19909 }
19910 return Err(self.err(alloc::format!(
19911 "expected ',' or ')' in {fn_name} column list, got {:?}",
19912 self.peek()
19913 )));
19914 }
19915 if coldefs.is_empty() {
19916 return Err(self.err(alloc::format!(
19917 "{fn_name} column-definition list must declare at least one column"
19918 )));
19919 }
19920 // Per column: (base ->> 'col')::type AS col. The base is the
19921 // per-element `value` column for the *set form, or the argument
19922 // itself for the scalar record form.
19923 let items: Vec<SelectItem> = coldefs
19924 .into_iter()
19925 .map(|(col, ty)| {
19926 let base = if is_set {
19927 Expr::Column(ColumnName {
19928 qualifier: None,
19929 name: "value".to_string(),
19930 })
19931 } else {
19932 arg.clone()
19933 };
19934 SelectItem::Expr {
19935 expr: Expr::Cast {
19936 expr: Box::new(Expr::Binary {
19937 lhs: Box::new(base),
19938 op: BinOp::JsonGetText,
19939 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19940 }),
19941 target: ty,
19942 },
19943 alias: Some(col),
19944 }
19945 })
19946 .collect();
19947 let from = if is_set {
19948 let elem_fn = if fn_name.starts_with("jsonb") {
19949 "jsonb_array_elements"
19950 } else {
19951 "json_array_elements"
19952 };
19953 Some(FromClause {
19954 primary: TableRef {
19955 name: "value".to_string(),
19956 alias: None,
19957 only: false,
19958 as_of_segment: None,
19959 unnest_expr: Some(Box::new(Expr::FunctionCall {
19960 name: elem_fn.to_string(),
19961 args: alloc::vec![arg],
19962 })),
19963 unnest_column_aliases: alloc::vec!["value".to_string()],
19964 with_ordinality: false,
19965 generate_series_args: None,
19966 lateral_subquery: None,
19967 jsonb_each_text_arg: None,
19968 table_fn_call: None,
19969 rows_from: None,
19970 json_table: None,
19971 scalar_fn_item: false,
19972 },
19973 joins: Vec::new(),
19974 })
19975 } else {
19976 None
19977 };
19978 let inner = SelectStatement {
19979 locking: None,
19980 ctes: Vec::new(),
19981 distinct: false,
19982 distinct_on: Vec::new(),
19983 items,
19984 from,
19985 where_: None,
19986 group_by: None,
19987 group_by_all: false,
19988 having: None,
19989 unions: Vec::new(),
19990 order_by: Vec::new(),
19991 limit: None,
19992 offset: None,
19993 limit_with_ties: false,
19994 window_check_exprs: Vec::new(),
19995 };
19996 Ok(TableRef {
19997 name: alias.clone(),
19998 alias: Some(alias),
19999 only: false,
20000 as_of_segment: None,
20001 unnest_expr: None,
20002 unnest_column_aliases: Vec::new(),
20003 with_ordinality: false,
20004 generate_series_args: None,
20005 lateral_subquery: Some(Box::new(inner)),
20006 jsonb_each_text_arg: None,
20007 table_fn_call: None,
20008 rows_from: None,
20009 json_table: None,
20010 scalar_fn_item: false,
20011 })
20012 }
20013
20014 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20015 /// Returns true when the clause was present. `WITH` alone (a
20016 /// CTE can never start here) is not enough — the ORDINALITY
20017 /// ident must follow, so a stray WITH still errors downstream.
20018 fn absorb_with_ordinality(&mut self) -> bool {
20019 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20020 && matches!(self.tokens.get(self.pos + 1),
20021 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20022 {
20023 self.advance();
20024 self.advance();
20025 true
20026 } else {
20027 false
20028 }
20029 }
20030
20031 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20032 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20033 /// Out-of-line: the caller sits on the FROM recursion chain.
20034 #[inline(never)]
20035 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20036 let fn_name = match self.advance() {
20037 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20038 _ => unreachable!("caller peeked an ident"),
20039 };
20040 self.advance(); // (
20041 let mut args: Vec<Expr> = Vec::new();
20042 if !matches!(self.peek(), Token::RParen) {
20043 loop {
20044 args.push(self.parse_expr(0)?);
20045 if matches!(self.peek(), Token::Comma) {
20046 self.advance();
20047 continue;
20048 }
20049 break;
20050 }
20051 }
20052 if !matches!(self.peek(), Token::RParen) {
20053 return Err(self.err(alloc::format!(
20054 "expected ')' after {fn_name}() arguments, got {:?}",
20055 self.peek()
20056 )));
20057 }
20058 self.advance();
20059 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20060 // counter column rides after the function's own, and the alias list
20061 // names it.
20062 let with_ordinality = self.absorb_with_ordinality();
20063 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20064 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20065 Ok(TableRef {
20066 name,
20067 alias: alias_ident,
20068 only: false,
20069 as_of_segment: None,
20070 unnest_expr: None,
20071 unnest_column_aliases,
20072 with_ordinality,
20073 generate_series_args: None,
20074 lateral_subquery: None,
20075 jsonb_each_text_arg: None,
20076 table_fn_call: Some(Box::new((fn_name, args))),
20077 rows_from: None,
20078 json_table: None,
20079 scalar_fn_item: false,
20080 })
20081 }
20082
20083 /// v7.39 (round 205, JSON_TABLE) — parse
20084 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20085 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20086 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20087 #[inline(never)]
20088 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20089 self.advance(); // json_table
20090 self.advance(); // (
20091 let doc = Box::new(self.parse_expr(0)?);
20092 self.expect_comma_json_table()?;
20093 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20094 // Optional `PASSING <expr> AS <name> [, …]`.
20095 let mut passing: Vec<(String, Expr)> = Vec::new();
20096 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20097 self.advance();
20098 loop {
20099 let e = self.parse_expr(0)?;
20100 if !matches!(self.peek(), Token::As) {
20101 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20102 }
20103 self.advance();
20104 let vname = match self.advance() {
20105 Token::Ident(s) | Token::QuotedIdent(s) => s,
20106 other => {
20107 return Err(self.err(alloc::format!(
20108 "expected PASSING variable name, got {other:?}"
20109 )));
20110 }
20111 };
20112 passing.push((vname, e));
20113 if matches!(self.peek(), Token::Comma) {
20114 self.advance();
20115 continue;
20116 }
20117 break;
20118 }
20119 }
20120 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20121 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20122 }
20123 self.advance();
20124 let columns = self.parse_json_table_columns()?;
20125 if !matches!(self.peek(), Token::RParen) {
20126 return Err(self.err(alloc::format!(
20127 "expected ')' to close JSON_TABLE, got {:?}",
20128 self.peek()
20129 )));
20130 }
20131 self.advance();
20132 let alias_ident = self.parse_optional_alias()?;
20133 let name = alias_ident
20134 .clone()
20135 .unwrap_or_else(|| String::from("json_table"));
20136 Ok(TableRef {
20137 name,
20138 alias: alias_ident,
20139 only: false,
20140 as_of_segment: None,
20141 unnest_expr: None,
20142 unnest_column_aliases: Vec::new(),
20143 with_ordinality: false,
20144 generate_series_args: None,
20145 lateral_subquery: None,
20146 jsonb_each_text_arg: None,
20147 table_fn_call: None,
20148 rows_from: None,
20149 json_table: Some(Box::new(crate::ast::JsonTable {
20150 doc,
20151 row_path,
20152 columns,
20153 passing,
20154 })),
20155 scalar_fn_item: false,
20156 })
20157 }
20158
20159 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20160 if !matches!(self.peek(), Token::Comma) {
20161 return Err(self.err(alloc::format!(
20162 "expected ',' after JSON_TABLE document, got {:?}",
20163 self.peek()
20164 )));
20165 }
20166 self.advance();
20167 Ok(())
20168 }
20169
20170 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20171 match self.advance() {
20172 Token::String(s) => Ok(s),
20173 other => Err(self.err(alloc::format!(
20174 "expected {what} string literal, got {other:?}"
20175 ))),
20176 }
20177 }
20178
20179 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20180 #[inline(never)]
20181 fn parse_json_table_columns(
20182 &mut self,
20183 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20184 if !matches!(self.peek(), Token::LParen) {
20185 return Err(self.err("expected '(' after COLUMNS".into()));
20186 }
20187 self.advance();
20188 let mut cols = Vec::new();
20189 loop {
20190 cols.push(self.parse_json_table_one_column()?);
20191 if matches!(self.peek(), Token::Comma) {
20192 self.advance();
20193 continue;
20194 }
20195 break;
20196 }
20197 if !matches!(self.peek(), Token::RParen) {
20198 return Err(self.err(alloc::format!(
20199 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20200 self.peek()
20201 )));
20202 }
20203 self.advance();
20204 Ok(cols)
20205 }
20206
20207 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20208 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20209 // NESTED [PATH] '<p>' COLUMNS (...)
20210 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20211 self.advance();
20212 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20213 self.advance();
20214 }
20215 let path = self.parse_json_string_literal("NESTED PATH")?;
20216 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20217 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20218 }
20219 self.advance();
20220 let columns = self.parse_json_table_columns()?;
20221 return Ok(JsonTableColumn::Nested { path, columns });
20222 }
20223 // <name> ...
20224 let name = match self.advance() {
20225 Token::Ident(s) | Token::QuotedIdent(s) => s,
20226 other => {
20227 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20228 }
20229 };
20230 // <name> FOR ORDINALITY
20231 if matches!(self.peek(), Token::For) {
20232 self.advance();
20233 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20234 return Err(self.err("expected ORDINALITY after FOR".into()));
20235 }
20236 self.advance();
20237 return Ok(JsonTableColumn::Ordinality { name });
20238 }
20239 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20240 let ty = self.parse_column_type_name()?;
20241 let mut format_json = false;
20242 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20243 self.advance();
20244 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20245 return Err(self.err("expected JSON after FORMAT".into()));
20246 }
20247 self.advance();
20248 format_json = true;
20249 }
20250 let mut exists = false;
20251 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20252 self.advance();
20253 exists = true;
20254 }
20255 let mut path = alloc::format!("$.{name}");
20256 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20257 self.advance();
20258 path = self.parse_json_string_literal("column PATH")?;
20259 }
20260 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20261 // `FORMAT JSON` after PATH (alternate placement).
20262 self.advance();
20263 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20264 self.advance();
20265 }
20266 format_json = true;
20267 }
20268 let mut wrapper = false;
20269 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20270 self.advance();
20271 // optional CONDITIONAL/UNCONDITIONAL
20272 if matches!(self.peek(), Token::Ident(s)
20273 if s.eq_ignore_ascii_case("unconditional")
20274 || s.eq_ignore_ascii_case("conditional"))
20275 {
20276 self.advance();
20277 }
20278 if !matches!(self.peek(), Token::Ident(s)
20279 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20280 {
20281 return Err(self.err("expected WRAPPER after WITH".into()));
20282 }
20283 self.advance();
20284 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20285 if matches!(self.peek(), Token::Ident(s)
20286 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20287 {
20288 self.advance();
20289 }
20290 wrapper = true;
20291 }
20292 // ON EMPTY / ON ERROR clauses (two, in any order).
20293 let mut on_empty = JsonTableOnBehavior::Null;
20294 let mut on_error = JsonTableOnBehavior::Null;
20295 for _ in 0..2 {
20296 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20297 {
20298 self.advance();
20299 Some(JsonTableOnBehavior::Error)
20300 } else if matches!(self.peek(), Token::Null) {
20301 self.advance();
20302 Some(JsonTableOnBehavior::Null)
20303 } else if matches!(self.peek(), Token::Default) {
20304 self.advance();
20305 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20306 } else {
20307 None
20308 };
20309 let Some(behavior) = behavior else { break };
20310 // `ON {EMPTY|ERROR}`
20311 if !matches!(self.peek(), Token::On) {
20312 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20313 }
20314 self.advance();
20315 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20316 self.advance();
20317 on_empty = behavior;
20318 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20319 self.advance();
20320 on_error = behavior;
20321 } else {
20322 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20323 }
20324 }
20325 Ok(JsonTableColumn::Regular {
20326 name,
20327 ty,
20328 path,
20329 exists,
20330 format_json,
20331 wrapper,
20332 on_empty,
20333 on_error,
20334 })
20335 }
20336
20337 fn parse_optional_alias_with_columns(
20338 &mut self,
20339 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20340 let alias = self.parse_optional_alias()?;
20341 if alias.is_none() {
20342 return Ok((None, Vec::new()));
20343 }
20344 let mut cols: Vec<String> = Vec::new();
20345 if matches!(self.peek(), Token::LParen) {
20346 self.advance();
20347 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20348 self.advance();
20349 cols.push(s);
20350 if matches!(self.peek(), Token::Comma) {
20351 self.advance();
20352 continue;
20353 }
20354 break;
20355 }
20356 if matches!(self.peek(), Token::RParen) {
20357 self.advance();
20358 }
20359 }
20360 Ok((alias, cols))
20361 }
20362
20363 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20364 /// whose keyword token was already consumed and whose `(` is the
20365 /// current token. Factored out of `parse_atom` (and marked
20366 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20367 /// recursive `parse_atom` frame — inlining them there enlarges the
20368 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20369 /// against, risking an overflow before the budget triggers.
20370 #[inline(never)]
20371 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20372 self.advance(); // (
20373 let mut args = Vec::new();
20374 if !matches!(self.peek(), Token::RParen) {
20375 loop {
20376 args.push(self.parse_expr(0)?);
20377 match self.peek() {
20378 Token::Comma => {
20379 self.advance();
20380 }
20381 Token::RParen => break,
20382 other => {
20383 return Err(self.err(alloc::format!(
20384 "expected ',' or ')' in {name}() args, got {other:?}"
20385 )));
20386 }
20387 }
20388 }
20389 }
20390 self.advance(); // )
20391 Ok(Expr::FunctionCall {
20392 name: name.into(),
20393 args,
20394 })
20395 }
20396
20397 /// FROM-clause: a primary table reference plus zero-or-more joined
20398 /// peers expressed via either `, <table>` (cross-product, no ON) or
20399 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20400 /// v1.10 keeps the join list flat (left-associative nested-loop
20401 /// semantics).
20402 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20403 let primary = self.parse_table_ref()?;
20404 let primary_qual = primary
20405 .alias
20406 .clone()
20407 .unwrap_or_else(|| primary.name.clone());
20408 let joins = self.parse_from_joins(&primary_qual)?;
20409 Ok(FromClause { primary, joins })
20410 }
20411
20412 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20413 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20414 /// SAME grammar after its target table has already been consumed.
20415 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20416 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20417 /// be parsed forward, once.)
20418 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20419 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20420 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20421 /// desugaring, which needs a name for the left side of each equality.
20422 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20423 let mut joins = Vec::new();
20424 loop {
20425 // `, <table>` — cross-product with no ON.
20426 if matches!(self.peek(), Token::Comma) {
20427 self.advance();
20428 let table = self.parse_table_ref()?;
20429 joins.push(FromJoin {
20430 kind: JoinKind::Cross,
20431 table,
20432 on: None,
20433 using_cols: None,
20434 natural: false,
20435 });
20436 continue;
20437 }
20438 // v7.37.16 — optional leading `NATURAL` before the join
20439 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20440 // not a lexer keyword (it arrives as a bare Ident), so match
20441 // it case-insensitively here. When present, no ON/USING
20442 // clause is allowed — the common columns are resolved at
20443 // execution time.
20444 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20445 if natural {
20446 self.advance();
20447 }
20448 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20449 // CROSS JOIN, and bare JOIN (defaults to INNER).
20450 let kind =
20451 match self.peek() {
20452 Token::Inner => {
20453 self.advance();
20454 if !matches!(self.peek(), Token::Join) {
20455 return Err(self
20456 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20457 }
20458 self.advance();
20459 JoinKind::Inner
20460 }
20461 Token::Left => {
20462 self.advance();
20463 if matches!(self.peek(), Token::Outer) {
20464 self.advance();
20465 }
20466 if !matches!(self.peek(), Token::Join) {
20467 return Err(self.err(format!(
20468 "expected JOIN after LEFT [OUTER], got {:?}",
20469 self.peek()
20470 )));
20471 }
20472 self.advance();
20473 JoinKind::Left
20474 }
20475 Token::Cross => {
20476 self.advance();
20477 if !matches!(self.peek(), Token::Join) {
20478 return Err(self
20479 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20480 }
20481 self.advance();
20482 JoinKind::Cross
20483 }
20484 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20485 Token::Right => {
20486 self.advance();
20487 if matches!(self.peek(), Token::Outer) {
20488 self.advance();
20489 }
20490 if !matches!(self.peek(), Token::Join) {
20491 return Err(self.err(format!(
20492 "expected JOIN after RIGHT [OUTER], got {:?}",
20493 self.peek()
20494 )));
20495 }
20496 self.advance();
20497 JoinKind::Right
20498 }
20499 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20500 Token::Full => {
20501 self.advance();
20502 if matches!(self.peek(), Token::Outer) {
20503 self.advance();
20504 }
20505 if !matches!(self.peek(), Token::Join) {
20506 return Err(self.err(format!(
20507 "expected JOIN after FULL [OUTER], got {:?}",
20508 self.peek()
20509 )));
20510 }
20511 self.advance();
20512 JoinKind::FullOuter
20513 }
20514 Token::Join => {
20515 self.advance();
20516 JoinKind::Inner
20517 }
20518 _ => break,
20519 };
20520 let table = self.parse_table_ref()?;
20521 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20522 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20523 // where prev_table is the most-recent left-side table
20524 // (the previous join's table if any, else the FROM primary).
20525 // PG semantics around column merging are richer (USING'd
20526 // cols become deduplicated single output columns); for
20527 // sugar purposes the predicate-only form covers the
20528 // baseline corpus shape and chained `… JOIN x USING (k)
20529 // JOIN y USING (k)` calls.
20530 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20531 // common columns resolve at execution time.
20532 if natural {
20533 joins.push(FromJoin {
20534 kind,
20535 table,
20536 on: None,
20537 using_cols: None,
20538 natural: true,
20539 });
20540 continue;
20541 }
20542 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20543 // v7.37.16 — capture the USING column list (in addition to
20544 // the ON desugar below) so the executor can perform PG's
20545 // column-merge on the output side.
20546 let mut using_cols: Option<Vec<String>> = None;
20547 let on = if matches!(self.peek(), Token::On) {
20548 self.advance();
20549 Some(self.parse_expr(0)?)
20550 } else if using_match {
20551 self.advance();
20552 if !matches!(self.peek(), Token::LParen) {
20553 return Err(
20554 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20555 );
20556 }
20557 self.advance();
20558 let mut cols: Vec<String> = Vec::new();
20559 loop {
20560 match self.peek().clone() {
20561 Token::Ident(s) | Token::QuotedIdent(s) => {
20562 self.advance();
20563 cols.push(s);
20564 }
20565 other => {
20566 return Err(self.err(format!(
20567 "expected column name inside USING (…), got {other:?}"
20568 )));
20569 }
20570 }
20571 match self.peek() {
20572 Token::Comma => {
20573 self.advance();
20574 continue;
20575 }
20576 Token::RParen => {
20577 self.advance();
20578 break;
20579 }
20580 other => {
20581 return Err(self.err(format!(
20582 "expected ',' or ')' inside USING (…), got {other:?}"
20583 )));
20584 }
20585 }
20586 }
20587 if cols.is_empty() {
20588 return Err(self.err("USING (…) requires at least one column".to_string()));
20589 }
20590 using_cols = Some(cols.clone());
20591 // Pick the left-side alias: prev join's table if any,
20592 // else FROM primary. Use alias when present, else
20593 // table name (PG-equivalent qualifier).
20594 let left_qual: String = joins
20595 .last()
20596 .map(|j| {
20597 j.table
20598 .alias
20599 .clone()
20600 .unwrap_or_else(|| j.table.name.clone())
20601 })
20602 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20603 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20604 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20605 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20606 qualifier: Some(left_qual.clone()),
20607 name: c.clone(),
20608 })),
20609 op: crate::ast::BinOp::Eq,
20610 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20611 qualifier: Some(right_qual.clone()),
20612 name: c,
20613 })),
20614 });
20615 let first = iter.next().expect("at least one col");
20616 Some(iter.fold(first, |acc, pred| Expr::Binary {
20617 lhs: alloc::boxed::Box::new(acc),
20618 op: crate::ast::BinOp::And,
20619 rhs: alloc::boxed::Box::new(pred),
20620 }))
20621 } else if kind == JoinKind::Cross {
20622 None
20623 } else {
20624 return Err(self.err(format!(
20625 "expected ON or USING after {:?} JOIN, got {:?}",
20626 kind,
20627 self.peek()
20628 )));
20629 };
20630 joins.push(FromJoin {
20631 kind,
20632 table,
20633 on,
20634 using_cols,
20635 natural: false,
20636 });
20637 }
20638 Ok(joins)
20639 }
20640
20641 /// Optional alias after an expression or table:
20642 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20643 /// accepted (PG-style implicit alias). Returns `None` if the next token
20644 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20645 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20646 if matches!(self.peek(), Token::As) {
20647 self.advance();
20648 // v7.39 (round 340, V56) — after AS the next token MUST be an
20649 // identifier. This used to return None and "let the caller
20650 // surface the error on the next expectation", but when AS is
20651 // the LAST token there is no next expectation: `SELECT 1 AS`
20652 // parsed clean and silently dropped the alias. PG rejects it.
20653 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20654 return self.expect_ident_like().map(Some);
20655 }
20656 return Err(self.err(alloc::format!(
20657 "expected an alias after AS, got {:?}",
20658 self.peek()
20659 )));
20660 }
20661 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20662 // grammar reserves a long list of follow-keywords from the
20663 // alias slot. SPG's bareword approximation: skip a small
20664 // set of idents that would otherwise be swallowed as the
20665 // table alias and break trailing clauses like CREATE
20666 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20667 // CONFLICT WHERE shapes.
20668 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20669 if is_alias_stopword(s) {
20670 return Ok(None);
20671 }
20672 return Ok(self.expect_ident_like().ok());
20673 }
20674 Ok(None)
20675 }
20676
20677 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20678 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20679 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20680 // error beats a stack overflow (an overflow aborts the
20681 // embedding host process).
20682 self.enter_nested()?;
20683 let r = self.parse_expr_inner(min_prec);
20684 self.nest_depth -= 1;
20685 r
20686 }
20687
20688 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20689 /// When the upcoming tokens form one, return the underlying
20690 /// operator token and the position just past the closing paren
20691 /// so the binary loop can dispatch on the plain operator.
20692 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20693 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20694 return None;
20695 }
20696 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20697 return None;
20698 }
20699 let mut i = self.pos + 2;
20700 // Optional schema qualifier (pg_catalog.<op> etc.).
20701 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20702 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20703 {
20704 i += 2;
20705 }
20706 let op_tok = self.tokens.get(i)?.clone();
20707 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20708 return None;
20709 }
20710 Some((i + 2, op_tok))
20711 }
20712
20713 /// PG operator symbols that lower onto function calls in
20714 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20715 /// family → regexp_like, comparison rung), `^@` (starts_with,
20716 /// comparison rung), `^` (power, tighter than `*`), `#`
20717 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20718 /// subset of the OR bits so the subtraction never borrows).
20719 fn try_symbol_operator(
20720 &mut self,
20721 lhs: &Expr,
20722 min_prec: u8,
20723 ) -> Result<Option<Expr>, ParseError> {
20724 enum Sym {
20725 Regex { ci: bool, negated: bool },
20726 Like { ci: bool, negated: bool },
20727 StartsWith,
20728 Power,
20729 Xor,
20730 RangeAdjacent,
20731 }
20732 // v7.39 (IS-precedence knife) — the low-precedence postfix
20733 // predicates ride this existing leaf call (zero new frame slots
20734 // on the nesting chain).
20735 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20736 return Ok(Some(e));
20737 }
20738 let (sym, prec): (Sym, u8) = match self.peek() {
20739 Token::Tilde => (
20740 Sym::Regex {
20741 ci: false,
20742 negated: false,
20743 },
20744 5,
20745 ),
20746 Token::TildeStar => (
20747 Sym::Regex {
20748 ci: true,
20749 negated: false,
20750 },
20751 5,
20752 ),
20753 Token::NotTilde => (
20754 Sym::Regex {
20755 ci: false,
20756 negated: true,
20757 },
20758 5,
20759 ),
20760 Token::NotTildeStar => (
20761 Sym::Regex {
20762 ci: true,
20763 negated: true,
20764 },
20765 5,
20766 ),
20767 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20768 Token::DoubleTilde => (
20769 Sym::Like {
20770 ci: false,
20771 negated: false,
20772 },
20773 5,
20774 ),
20775 Token::DoubleTildeStar => (
20776 Sym::Like {
20777 ci: true,
20778 negated: false,
20779 },
20780 5,
20781 ),
20782 Token::NotDoubleTilde => (
20783 Sym::Like {
20784 ci: false,
20785 negated: true,
20786 },
20787 5,
20788 ),
20789 Token::NotDoubleTildeStar => (
20790 Sym::Like {
20791 ci: true,
20792 negated: true,
20793 },
20794 5,
20795 ),
20796 Token::CaretAt => (Sym::StartsWith, 5),
20797 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20798 // tighter than `* / & |`, which the prec-9 rung preserves —
20799 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20800 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20801 Token::Caret => (Sym::Power, 9),
20802 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20803 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20804 Token::Hash => (Sym::Xor, 6),
20805 Token::Adjacent => (Sym::RangeAdjacent, 5),
20806 _ => return Ok(None),
20807 };
20808 if prec < min_prec {
20809 return Ok(None);
20810 }
20811 self.advance();
20812 let rhs = self.parse_expr(prec + 1)?;
20813 let out = match sym {
20814 Sym::Regex { ci, negated } => {
20815 let mut args = alloc::vec![lhs.clone(), rhs];
20816 if ci {
20817 args.push(Expr::Literal(Literal::String(String::from("i"))));
20818 }
20819 maybe_not(
20820 Expr::FunctionCall {
20821 name: String::from("regexp_like"),
20822 args,
20823 },
20824 negated,
20825 )
20826 }
20827 Sym::Like { ci, negated } => Expr::Like {
20828 expr: alloc::boxed::Box::new(lhs.clone()),
20829 pattern: alloc::boxed::Box::new(rhs),
20830 negated,
20831 case_insensitive: ci,
20832 },
20833 Sym::StartsWith => Expr::FunctionCall {
20834 name: String::from("starts_with"),
20835 args: alloc::vec![lhs.clone(), rhs],
20836 },
20837 Sym::Power => Expr::FunctionCall {
20838 name: String::from("power"),
20839 args: alloc::vec![lhs.clone(), rhs],
20840 },
20841 // `#` bitwise XOR — a real operator now (was desugared to
20842 // `(a|b)-(a&b)`, algebraically identical for integers but
20843 // undefined for bit strings; the direct op handles both).
20844 Sym::Xor => Expr::Binary {
20845 lhs: Box::new(lhs.clone()),
20846 op: BinOp::BitXor,
20847 rhs: Box::new(rhs),
20848 },
20849 // range `-|-` "is adjacent to" — lowered to a catalog function.
20850 Sym::RangeAdjacent => Expr::FunctionCall {
20851 name: String::from("range_adjacent"),
20852 args: alloc::vec![lhs.clone(), rhs],
20853 },
20854 };
20855 Ok(Some(out))
20856 }
20857
20858 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20859 /// predicates, moved out of the tight postfix-cast loop: PG binds
20860 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20861 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20862 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20863 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20864 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20865 /// when nothing at this position belongs to the family. Out-of-line
20866 /// (`inline(never)`): the caller sits on the per-nesting-level frame
20867 /// chain that MAX_NEST_DEPTH is tuned against.
20868 #[inline(never)]
20869 fn parse_postfix_predicate(
20870 &mut self,
20871 lhs: &Expr,
20872 min_prec: u8,
20873 ) -> Result<Option<Expr>, ParseError> {
20874 // Reached through try_symbol_operator (an existing leaf call of
20875 // the binary loop) so NO new stack slots land on the per-nesting
20876 // frame chain; the lhs clones only when a predicate actually
20877 // consumes it.
20878 match self.peek() {
20879 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20880 // comparison family rung 5 (each +1 from the pre-XOR ladder).
20881 Token::Is if min_prec <= 4 => {}
20882 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20883 Token::Not
20884 if min_prec <= 5
20885 && matches!(
20886 self.tokens.get(self.pos + 1),
20887 Some(Token::Between | Token::In | Token::Like)
20888 ) => {}
20889 Token::Not | Token::Ident(_)
20890 if min_prec <= 5
20891 && (matches!(self.peek(), Token::Ident(s)
20892 if s.eq_ignore_ascii_case("ilike")
20893 || (self.mysql_dialect
20894 && (s.eq_ignore_ascii_case("regexp")
20895 || s.eq_ignore_ascii_case("rlike")))
20896 || (s.eq_ignore_ascii_case("similar")
20897 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20898 || (matches!(self.peek(), Token::Not)
20899 && matches!(self.tokens.get(self.pos + 1),
20900 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20901 || (self.mysql_dialect
20902 && (s.eq_ignore_ascii_case("regexp")
20903 || s.eq_ignore_ascii_case("rlike")))
20904 || s.eq_ignore_ascii_case("similar")))) => {}
20905 _ => return Ok(None),
20906 }
20907 let mut expr = lhs.clone();
20908 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20909 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20910 if min_prec <= 4 {
20911 if matches!(self.peek(), Token::Is) {
20912 self.advance();
20913 let negated = if matches!(self.peek(), Token::Not) {
20914 self.advance();
20915 true
20916 } else {
20917 false
20918 };
20919 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20920 // mailrs pg_dump.
20921 if matches!(self.peek(), Token::Distinct) {
20922 self.advance();
20923 if !matches!(self.peek(), Token::From) {
20924 return Err(self.err(format!(
20925 "expected FROM after IS{} DISTINCT, got {:?}",
20926 if negated { " NOT" } else { "" },
20927 self.peek()
20928 )));
20929 }
20930 self.advance();
20931 // Right-hand side: parse at the same precedence
20932 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20933 // groups as `x IS DISTINCT FROM (a + b)`.
20934 let rhs = self.parse_expr(5)?;
20935 let op = if negated {
20936 BinOp::IsNotDistinctFrom
20937 } else {
20938 BinOp::IsDistinctFrom
20939 };
20940 expr = Expr::Binary {
20941 op,
20942 lhs: Box::new(expr),
20943 rhs: Box::new(rhs),
20944 };
20945 {
20946 return Ok(Some(expr));
20947 }
20948 }
20949 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20950 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20951 // Lowers onto pg_is_json(x, kind); NOT wraps the
20952 // call in a logical negation.
20953 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20954 if s.eq_ignore_ascii_case("json"))
20955 {
20956 self.advance(); // JSON
20957 let kind = match self.peek() {
20958 Token::Ident(s) | Token::QuotedIdent(s)
20959 if matches!(
20960 s.to_ascii_lowercase().as_str(),
20961 "value" | "object" | "array" | "scalar"
20962 ) =>
20963 {
20964 let k = s.to_ascii_lowercase();
20965 self.advance();
20966 k
20967 }
20968 _ => "value".to_string(),
20969 };
20970 let call = Expr::FunctionCall {
20971 name: "pg_is_json".to_string(),
20972 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20973 };
20974 expr = if negated {
20975 Expr::Unary {
20976 op: UnOp::Not,
20977 expr: Box::new(call),
20978 }
20979 } else {
20980 call
20981 };
20982 {
20983 return Ok(Some(expr));
20984 }
20985 }
20986 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
20987 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
20988 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
20989 {
20990 let form_kw = match self.peek() {
20991 Token::Ident(s) | Token::QuotedIdent(s)
20992 if matches!(
20993 s.to_ascii_uppercase().as_str(),
20994 "NFC" | "NFD" | "NFKC" | "NFKD"
20995 ) && matches!(
20996 self.tokens.get(self.pos + 1),
20997 Some(Token::Ident(n) | Token::QuotedIdent(n))
20998 if n.eq_ignore_ascii_case("normalized")
20999 ) =>
21000 {
21001 Some(s.to_ascii_uppercase())
21002 }
21003 _ => None,
21004 };
21005 let bare_normalized = form_kw.is_none()
21006 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21007 if s.eq_ignore_ascii_case("normalized"));
21008 if form_kw.is_some() || bare_normalized {
21009 if form_kw.is_some() {
21010 self.advance(); // form keyword
21011 }
21012 self.advance(); // NORMALIZED
21013 let mut args = alloc::vec![expr];
21014 if let Some(f) = form_kw {
21015 args.push(Expr::Literal(Literal::String(f)));
21016 }
21017 let call = Expr::FunctionCall {
21018 name: "is_normalized".to_string(),
21019 args,
21020 };
21021 expr = if negated {
21022 Expr::Unary {
21023 op: UnOp::Not,
21024 expr: Box::new(call),
21025 }
21026 } else {
21027 call
21028 };
21029 {
21030 return Ok(Some(expr));
21031 }
21032 }
21033 }
21034 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21035 // three-valued boolean tests. IS TRUE/FALSE never
21036 // return NULL, so they lower to CASE forms whose
21037 // ELSE catches the NULL branch; IS UNKNOWN on a
21038 // boolean is exactly IS NULL.
21039 if matches!(self.peek(), Token::True | Token::False)
21040 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21041 {
21042 let tok = self.advance();
21043 let test = match tok {
21044 Token::True => Some(true),
21045 Token::False => Some(false),
21046 _ => None, // UNKNOWN
21047 };
21048 // v7.39 (round 328, V45) — kept as what the user
21049 // wrote. These used to be lowered here into `CASE` /
21050 // `IS NULL`; the semantics were right but the AST no
21051 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21052 // was echoed back as
21053 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21054 expr = Expr::BoolTest {
21055 expr: Box::new(expr),
21056 value: test,
21057 negated,
21058 };
21059 {
21060 return Ok(Some(expr));
21061 }
21062 }
21063 if !matches!(self.peek(), Token::Null) {
21064 return Err(self.err(format!(
21065 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21066 if negated { " NOT" } else { "" },
21067 self.peek()
21068 )));
21069 }
21070 self.advance();
21071 expr = Expr::IsNull {
21072 expr: Box::new(expr),
21073 negated,
21074 };
21075 {
21076 return Ok(Some(expr));
21077 }
21078 }
21079 }
21080 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21081 if min_prec <= 5 {
21082 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21083 // Look one token ahead so a stray `NOT` not followed by any of
21084 // these flows through to the early return below untouched.
21085 let negated = if matches!(self.peek(), Token::Not) {
21086 let next = self.tokens.get(self.pos + 1);
21087 matches!(next, Some(Token::Between | Token::In | Token::Like))
21088 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21089 || (self.mysql_dialect
21090 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21091 || s.eq_ignore_ascii_case("similar"))
21092 } else {
21093 false
21094 };
21095 if negated {
21096 self.advance();
21097 }
21098 if matches!(self.peek(), Token::Between) {
21099 expr = self.parse_between_tail(expr, negated)?;
21100 {
21101 return Ok(Some(expr));
21102 }
21103 }
21104 if matches!(self.peek(), Token::In) {
21105 if self.suppress_in_tail && !negated {
21106 // POSITION(sub IN str) — IN belongs to the
21107 // enclosing function syntax; stop here.
21108 {
21109 return Ok(None);
21110 }
21111 }
21112 expr = self.parse_in_tail(expr, negated)?;
21113 {
21114 return Ok(Some(expr));
21115 }
21116 }
21117 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21118 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21119 // (the SQL→regex transform runs inside, in the backtracking-
21120 // friendly shape SPG's matcher needs).
21121 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21122 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21123 {
21124 self.advance(); // SIMILAR
21125 self.advance(); // TO
21126 let pattern = self.parse_expr(6)?;
21127 let mut args = alloc::vec![expr, pattern];
21128 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21129 self.advance();
21130 args.push(self.parse_expr(6)?);
21131 }
21132 let call = Expr::FunctionCall {
21133 name: "__similar_to".to_string(),
21134 args,
21135 };
21136 expr = maybe_not(call, negated);
21137 {
21138 return Ok(Some(expr));
21139 }
21140 }
21141 if matches!(self.peek(), Token::Like) {
21142 self.advance();
21143 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21144 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21145 expr = q;
21146 {
21147 return Ok(Some(expr));
21148 }
21149 }
21150 // Pattern at the same precedence as other comparison RHSes —
21151 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21152 let mut pattern = self.parse_expr(6)?;
21153 // `ESCAPE 'c'` — rewrite a literal pattern to the
21154 // default backslash escape at parse time. Custom
21155 // escapes on non-literal patterns would need
21156 // matcher support; error honestly.
21157 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21158 self.advance();
21159 let esc = self.parse_expr(6)?;
21160 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21161 }
21162 expr = Expr::Like {
21163 expr: Box::new(expr),
21164 pattern: Box::new(pattern),
21165 negated,
21166 case_insensitive: false,
21167 };
21168 {
21169 return Ok(Some(expr));
21170 }
21171 }
21172 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21173 // keyword reaches us as a plain identifier.
21174 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21175 self.advance();
21176 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21177 expr = q;
21178 {
21179 return Ok(Some(expr));
21180 }
21181 }
21182 let pattern = self.parse_expr(6)?;
21183 expr = Expr::Like {
21184 expr: Box::new(expr),
21185 pattern: Box::new(pattern),
21186 negated,
21187 case_insensitive: true,
21188 };
21189 {
21190 return Ok(Some(expr));
21191 }
21192 }
21193 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21194 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21195 // matches case-insensitively under the default collation, so it
21196 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21197 // `~*` operator uses, wrapped in NOT when negated.
21198 if self.mysql_dialect
21199 && matches!(self.peek(), Token::Ident(s)
21200 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21201 {
21202 self.advance();
21203 let pattern = self.parse_expr(6)?;
21204 let call = Expr::FunctionCall {
21205 name: String::from("regexp_like"),
21206 args: alloc::vec![
21207 expr,
21208 pattern,
21209 Expr::Literal(Literal::String(String::from("i"))),
21210 ],
21211 };
21212 return Ok(Some(maybe_not(call, negated)));
21213 }
21214 }
21215 let _ = expr;
21216 Ok(None)
21217 }
21218
21219 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21220 let mut lhs = self.parse_unary()?;
21221 let mut chain_len = 0usize;
21222 loop {
21223 // OPERATOR([schema.]op) reduces to its underlying
21224 // operator token before the normal dispatch.
21225 let explicit = self.peek_explicit_operator();
21226 let dispatch = match &explicit {
21227 Some((_, tok)) => self.binop_here(tok),
21228 None => self.binop_here(self.peek()),
21229 };
21230 let Some((op, prec)) = dispatch else {
21231 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21232 // of the symbol family. `binop_here` answers None for them
21233 // because they lower onto function calls rather than a
21234 // BinOp, and the fallback below reads `self.peek()` — the
21235 // word OPERATOR, not the operator. `pg_dump` writes every
21236 // catalog predicate this way, so its first query failed
21237 // and no dump ran:
21238 //
21239 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21240 //
21241 // Collapsing the wrapper to the operator it names puts the
21242 // token where the fallback already looks.
21243 if let Some((next, op_tok)) = explicit {
21244 self.tokens.splice(self.pos..next, [op_tok]);
21245 }
21246 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21247 lhs = e;
21248 chain_len += 1;
21249 if chain_len > MAX_BINARY_CHAIN {
21250 return Err(self.err(alloc::format!(
21251 "more than {MAX_BINARY_CHAIN} chained binary operators"
21252 )));
21253 }
21254 continue;
21255 }
21256 break;
21257 };
21258 if prec < min_prec {
21259 break;
21260 }
21261 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21262 // iteratively but evaluates and drops recursively;
21263 // depth beyond the budget overflows worker stacks.
21264 chain_len += 1;
21265 if chain_len > MAX_BINARY_CHAIN {
21266 return Err(self.err(alloc::format!(
21267 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21268 )));
21269 }
21270 match explicit {
21271 Some((end_pos, _)) => self.pos = end_pos,
21272 None => {
21273 self.advance();
21274 }
21275 }
21276 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21277 // ANY is a bare ident; ALL is a reserved Token. Both
21278 // require an immediate `(` to disambiguate from
21279 // identifier columns named `any` / `all`.
21280 let any_kind = match self.peek() {
21281 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21282 Some(false)
21283 }
21284 Token::Ident(s) | Token::QuotedIdent(s)
21285 if (s.eq_ignore_ascii_case("any")
21286 || s.eq_ignore_ascii_case("some")
21287 || s.eq_ignore_ascii_case("all"))
21288 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21289 {
21290 Some(!s.eq_ignore_ascii_case("all"))
21291 }
21292 _ => None,
21293 };
21294 if let Some(is_any) = any_kind {
21295 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21296 continue;
21297 }
21298 let rhs = self.parse_expr(prec + 1)?;
21299 lhs = Expr::Binary {
21300 lhs: Box::new(lhs),
21301 op,
21302 rhs: Box::new(rhs),
21303 };
21304 }
21305 Ok(lhs)
21306 }
21307
21308 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21309 /// and the array form.
21310 ///
21311 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21312 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21313 /// this block's `Expr` temporaries and four `format!` sites slots in
21314 /// that frame on every level of `((((1))))`, which never reaches it.
21315 #[inline(never)]
21316 fn parse_any_all_rhs(
21317 &mut self,
21318 lhs: Expr,
21319 op: BinOp,
21320 is_any: bool,
21321 ) -> Result<Expr, ParseError> {
21322 self.advance(); // ident
21323 self.advance(); // (
21324 // `x op ANY (SELECT …)` — the quantified-subquery
21325 // form. `= ANY` is exactly IN; the other operators
21326 // lower onto EXISTS over the subquery as a derived
21327 // table, comparing against its single projection
21328 // aliased __v (x's columns resolve correlated).
21329 // ALL is the negated-EXISTS complement; a NULL
21330 // element makes PG return NULL where this lowering
21331 // returns true — the NOT NULL column case (the
21332 // practical one) is exact.
21333 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21334 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21335 // legal PG too (round-151 sibling). Out-of-line
21336 // (#[inline(never)] helper) — this sits on
21337 // parse_expr's recursive frame and the two-armed
21338 // SELECT temporary blew the nesting-budget stack.
21339 let mut sub = self.parse_any_all_select_body()?;
21340 if !matches!(self.peek(), Token::RParen) {
21341 return Err(self.err(alloc::format!(
21342 "expected ')' after ANY/ALL subquery, got {:?}",
21343 self.peek()
21344 )));
21345 }
21346 self.advance();
21347 if sub.items.len() != 1 {
21348 return Err(self.err(alloc::format!(
21349 "ANY/ALL subquery must return one column, got {}",
21350 sub.items.len()
21351 )));
21352 }
21353 if is_any && matches!(op, BinOp::Eq) {
21354 return Ok(Expr::InSubquery {
21355 expr: Box::new(lhs),
21356 subquery: Box::new(sub),
21357 negated: false,
21358 });
21359 }
21360 // The engine's subquery resolvers materialise
21361 // the single-column result into an ARRAY the
21362 // existing AnyAll three-valued eval consumes.
21363 return Ok(Expr::AnyAll {
21364 expr: Box::new(lhs),
21365 op,
21366 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21367 is_any,
21368 });
21369 }
21370 let arr = self.parse_expr(0)?;
21371 if !matches!(self.peek(), Token::RParen) {
21372 return Err(self.err(alloc::format!(
21373 "expected ')' after ANY/ALL argument, got {:?}",
21374 self.peek()
21375 )));
21376 }
21377 self.advance();
21378 Ok(Expr::AnyAll {
21379 expr: Box::new(lhs),
21380 op,
21381 array: Box::new(arr),
21382 is_any,
21383 })
21384 }
21385
21386 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21387 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21388 #[inline(never)]
21389 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21390 self.advance();
21391 let e = self.parse_expr(9)?;
21392 Ok(build_center_call(e))
21393 }
21394
21395 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21396 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21397 /// unary minus.
21398 ///
21399 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21400 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21401 /// the Expr-sized local stays out of that frame.
21402 #[inline(never)]
21403 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21404 self.advance();
21405 let e = self.parse_expr(9)?;
21406 Ok(Expr::FunctionCall {
21407 name: alloc::string::String::from(name),
21408 args: alloc::vec![e],
21409 })
21410 }
21411
21412 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21413 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21414 #[inline(never)]
21415 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21416 self.advance();
21417 let e = self.parse_expr(9)?;
21418 Ok(Expr::FunctionCall {
21419 name: alloc::string::String::from(if vertical {
21420 "isvertical"
21421 } else {
21422 "ishorizontal"
21423 }),
21424 args: alloc::vec![e],
21425 })
21426 }
21427
21428 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21429 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21430 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21431 #[inline(never)]
21432 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21433 self.advance();
21434 let e = self.parse_expr(9)?;
21435 Ok(Expr::Cast {
21436 expr: Box::new(e),
21437 target: CastTarget::Named("binary".to_string()),
21438 })
21439 }
21440
21441 /// The prefix operators that share one shape: take the token, parse
21442 /// an operand at `prec`, wrap it.
21443 ///
21444 /// `#[inline(never)]`, and one function instead of five arms, for the
21445 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21446 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21447 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21448 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21449 /// five `Expr`-sized locals per level for them anyway.
21450 #[inline(never)]
21451 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21452 self.advance();
21453 let e = self.parse_expr(prec)?;
21454 Ok(Expr::Unary {
21455 op,
21456 expr: Box::new(e),
21457 })
21458 }
21459
21460 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21461 /// and separate from it because of the literal folding below and the
21462 /// `format!` temporaries that folding needs.
21463 #[inline(never)]
21464 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21465 self.advance();
21466 // v7.39 (round 549) — fold the sign into an integer literal that
21467 // only fits once it is negative.
21468 //
21469 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21470 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21471 // folds the sign first, so `-9223372036854775808` is a bigint
21472 // there — and `-9223372036854775808 - 1` raises "bigint out of
21473 // range" where SPG quietly answered -9223372036854775809, a value
21474 // no bigint can hold. The arithmetic itself was already checked;
21475 // only the literal's type was wrong.
21476 if let Token::Numeric(lit) = self.peek()
21477 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21478 {
21479 self.advance();
21480 return Ok(Expr::Literal(Literal::Integer(folded)));
21481 }
21482 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21483 // `<->` slotted into 5 and arithmetic shifted up).
21484 let e = self.parse_expr(9)?;
21485 Ok(Expr::Unary {
21486 op: UnOp::Neg,
21487 expr: Box::new(e),
21488 })
21489 }
21490
21491 /// tsquery `!!` prefix negation, lowered to the catalog function.
21492 /// Binds like unary minus. Out-of-line for the frame reason on
21493 /// `parse_unary_op`.
21494 #[inline(never)]
21495 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21496 self.advance();
21497 let e = self.parse_expr(9)?;
21498 Ok(Expr::FunctionCall {
21499 name: String::from("tsquery_not"),
21500 args: alloc::vec![e],
21501 })
21502 }
21503
21504 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21505 match self.peek() {
21506 // NOT binds tighter than AND / XOR / OR but looser than
21507 // comparisons — its operand takes everything ≥ the comparison
21508 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21509 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21510 // was rung 3, behaviour-identical when 3 was unused; AND now
21511 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21512 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21513 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21514 // The body is out-of-line: `parse_unary` is one of the three
21515 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21516 // inline arm here overflowed the native stack in
21517 // `nesting_budget_errors_cleanly` — the guard test caught it,
21518 // exactly as the eval-side cliff did in rounds 346 and 351.
21519 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21520 self.parse_binary_prefix()
21521 }
21522 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21523 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21524 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21525 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21526 Token::Minus => self.parse_prefix_minus(),
21527 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21528 // worked only because the lexer reads it as one signed literal;
21529 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21530 // PG18 and MariaDB take all of them. Binds like unary minus.
21531 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21532 // Bitwise NOT binds like unary minus.
21533 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21534 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21535 // "center of" operator; desugars to center(x). The whole arm
21536 // is out-of-line: parse_unary sits on the per-nesting-level
21537 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21538 // Expr-sized local may live in this frame.
21539 Token::TsMatch => self.parse_prefix_center(),
21540 // v7.39 (round 508) — the prefix operators that are named
21541 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21542 // is length. Out-of-line for the same nesting-frame reason as
21543 // parse_prefix_center — parse_unary sits on the recursive cycle
21544 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21545 // live in this frame.
21546 Token::At => self.parse_prefix_call("abs"),
21547 Token::Hash => self.parse_prefix_call("npoints"),
21548 Token::AtMinusAt => self.parse_prefix_call("length"),
21549 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21550 // "is horizontal" (lseg / line); desugars to the existing
21551 // isvertical()/ishorizontal() functions. Out-of-line for the
21552 // same nesting-frame reason as parse_prefix_center.
21553 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21554 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21555 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21556 _ => self.parse_atom(),
21557 }
21558 }
21559
21560 /// Parse a parenthesised scalar subquery body after the caller has consumed
21561 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21562 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21563 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21564 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21565 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21566 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21567 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21568 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21569 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21570 /// tips the deep-nesting test into a stack overflow).
21571 #[inline(never)]
21572 fn array_subquery_ahead(&self) -> bool {
21573 if !matches!(self.peek(), Token::LParen) {
21574 return false;
21575 }
21576 matches!(
21577 self.tokens.get(self.pos + 1),
21578 Some(Token::Select | Token::Values)
21579 ) || matches!(
21580 self.tokens.get(self.pos + 1),
21581 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21582 )
21583 }
21584
21585 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21586 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21587 /// locals stay off parse_atom's recursive frame (round 105).
21588 #[inline(never)]
21589 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21590 self.advance(); // consume `[`
21591 let mut items: Vec<Expr> = Vec::new();
21592 if !matches!(self.peek(), Token::RBracket) {
21593 loop {
21594 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21595 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21596 if matches!(self.peek(), Token::LBracket) {
21597 items.push(self.parse_array_bracket_body()?);
21598 } else {
21599 items.push(self.parse_expr(0)?);
21600 }
21601 match self.peek() {
21602 Token::Comma => {
21603 self.advance();
21604 }
21605 Token::RBracket => break,
21606 other => {
21607 return Err(self.err(alloc::format!(
21608 "expected ',' or ']' in ARRAY literal, got {other:?}"
21609 )));
21610 }
21611 }
21612 }
21613 }
21614 self.advance(); // consume `]`
21615 Ok(Expr::Array(items))
21616 }
21617
21618 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21619 /// is already consumed; the current token is `(`. Desugars to a scalar
21620 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21621 /// the subquery's single-column rows in order — reusing the existing
21622 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21623 /// keeps the large `Statement` local off parse_atom's recursive frame.
21624 #[inline(never)]
21625 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21626 self.advance(); // consume `(`
21627 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21628 if w.eq_ignore_ascii_case("with"));
21629 let sub = if is_with {
21630 self.advance(); // WITH
21631 self.parse_with_cte_then_select()?
21632 } else {
21633 self.parse_select_stmt()?
21634 };
21635 if !matches!(self.peek(), Token::RParen) {
21636 return Err(self.err(alloc::format!(
21637 "expected ')' to close ARRAY(subquery), got {:?}",
21638 self.peek()
21639 )));
21640 }
21641 self.advance(); // consume `)`
21642 // Reuse the parser to build the array_agg wrapper from the subquery's
21643 // canonical text — avoids hand-constructing the derived-table AST.
21644 let wrapper = alloc::format!(
21645 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21646 );
21647 let stmt = parse_statement(&wrapper)
21648 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21649 let Statement::Select(sel) = stmt else {
21650 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21651 };
21652 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21653 }
21654
21655 #[inline(never)]
21656 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21657 let inner = if is_with {
21658 self.advance(); // WITH
21659 self.parse_with_cte_then_select()?
21660 } else {
21661 self.parse_select_stmt()?
21662 };
21663 match self.advance() {
21664 Token::RParen => {
21665 let Statement::Select(s) = inner else {
21666 return Err(ParseError {
21667 message: "scalar subquery body must be a SELECT".into(),
21668 token_pos: self.consumed_pos(),
21669 });
21670 };
21671 Ok(Expr::ScalarSubquery(Box::new(s)))
21672 }
21673 other => Err(ParseError {
21674 message: format!("expected ')' after scalar subquery, got {other:?}"),
21675 token_pos: self.consumed_pos(),
21676 }),
21677 }
21678 }
21679
21680 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21681 /// literals. The lexer splits them into an ident + string; recombine
21682 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21683 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21684 /// frame for the `body` / `bits` strings and their char loops (the
21685 /// round-367 frame cliff, M20).
21686 #[inline(never)]
21687 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21688 let is_hex = match self.peek() {
21689 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21690 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21691 _ => return None,
21692 };
21693 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21694 return None;
21695 }
21696 self.advance();
21697 let Token::String(body) = self.advance() else {
21698 unreachable!("guarded above");
21699 };
21700 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21701 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21702 // (hex pairs, even count required — MariaDB errors on an odd
21703 // count); `b'1010'` packs its bits big-endian, left-padded to a
21704 // byte. Lower both onto the bytea cast.
21705 if self.mysql_dialect {
21706 if is_hex {
21707 if body.len() % 2 == 1 {
21708 return Some(Err(self.err(alloc::format!(
21709 "invalid hex string literal X'{body}': odd digit count"
21710 ))));
21711 }
21712 for c in body.chars() {
21713 if !c.is_ascii_hexdigit() {
21714 return Some(Err(
21715 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21716 ));
21717 }
21718 }
21719 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21720 }
21721 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21722 return Some(Err(
21723 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21724 ));
21725 }
21726 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21727 }
21728 let bits = if is_hex {
21729 let mut out = String::with_capacity(body.len() * 4);
21730 for c in body.chars() {
21731 let Some(d) = c.to_digit(16) else {
21732 return Some(Err(self.err(alloc::format!(
21733 "invalid hexadecimal digit {c:?} in X'…' bit string"
21734 ))));
21735 };
21736 out.push_str(&alloc::format!("{d:04b}"));
21737 }
21738 out
21739 } else {
21740 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21741 return Some(Err(self.err(alloc::format!(
21742 "invalid binary digit {bad:?} in B'…' bit string"
21743 ))));
21744 }
21745 body
21746 };
21747 // Route through the postfix-cast loop so a chained cast like
21748 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21749 // of erroring at the `::`.
21750 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21751 // literal keeps its exact length, while an explicit `::bit` cast is
21752 // bit(1) with pad/truncate semantics (PG).
21753 Some(self.finish_postfix_casts(Expr::Cast {
21754 expr: Box::new(Expr::Literal(Literal::String(bits))),
21755 target: CastTarget::Named("__bit_literal".to_string()),
21756 }))
21757 }
21758
21759 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21760 if let Some(res) = self.try_parse_bit_string_literal() {
21761 return res;
21762 }
21763 let tok_pos = self.pos;
21764 match self.advance() {
21765 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21766 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21767 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21768 // carrying the source mantissa + scale so no precision is lost. A
21769 // literal too wide for i128 falls back to double precision.
21770 // Out-of-line (#[inline(never)]) — this arm sits on the
21771 // parse_expr recursion chain; its expansion locals must not
21772 // widen the recursive frame (debug frame-cliff discipline).
21773 Token::Numeric(s) => match numeric_token_to_literal(s) {
21774 Ok(lit) => Ok(Expr::Literal(lit)),
21775 Err(msg) => Err(self.err(msg)),
21776 },
21777 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21778 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21779 // (the lexer only emits this token in the MySQL dialect). Lower
21780 // onto the existing bytea cast; out-of-line to keep this arm off
21781 // the parse recursion frame.
21782 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21783 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21784 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21785 Token::Null => Ok(Expr::Literal(Literal::Null)),
21786 // v6.1.1 — `$N` placeholder. The actual Value lookup
21787 // happens in the engine eval path against the prepared-
21788 // statement bind buffer.
21789 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21790 Token::LParen => {
21791 // v4.10: `(SELECT ...)` in expression position is a
21792 // scalar subquery; otherwise it's a parenthesised
21793 // expression. Peek for SELECT keyword to dispatch.
21794 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21795 // lexes as Ident("with") (not a reserved token). The subquery body
21796 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21797 // so its large `Statement` local stays out of parse_atom's stack
21798 // frame — parse_atom is on the recursive `((…))` cycle and the
21799 // nesting budget is tuned to its frame size).
21800 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21801 if s.eq_ignore_ascii_case("with"));
21802 if matches!(self.peek(), Token::Select) || is_with {
21803 self.parse_paren_scalar_subquery(is_with)
21804 } else {
21805 let e = self.parse_expr(0)?;
21806 // `(a, b, …)` — a row constructor. Valid only
21807 // in front of a comparison operator or [NOT]
21808 // IN; both expand at parse time (lexicographic
21809 // comparison / OR'd row equalities).
21810 if matches!(self.peek(), Token::Comma) {
21811 let mut row = alloc::vec![e];
21812 while matches!(self.peek(), Token::Comma) {
21813 self.advance();
21814 row.push(self.parse_expr(0)?);
21815 }
21816 if !matches!(self.peek(), Token::RParen) {
21817 return Err(self.err(alloc::format!(
21818 "expected ')' after row constructor, got {:?}",
21819 self.peek()
21820 )));
21821 }
21822 self.advance();
21823 // A bare `(a, b, …)` row constructor can carry postfix
21824 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21825 // early return here skips parse_atom's tail postfix
21826 // pass, so fold casts in explicitly. For the
21827 // comparison / predicate forms nothing postfix follows,
21828 // so this is a no-op.
21829 return self
21830 .parse_row_comparison_tail(row)
21831 .and_then(|e| self.finish_postfix_casts(e));
21832 }
21833 match self.advance() {
21834 Token::RParen => Ok(e),
21835 other => Err(ParseError {
21836 message: format!("expected ')', got {other:?}"),
21837 token_pos: self.consumed_pos(),
21838 }),
21839 }
21840 }
21841 }
21842 Token::LBracket => self.parse_vector_literal_body(),
21843 Token::Extract => self.parse_extract_atom(),
21844 Token::Interval => self.parse_interval_atom(),
21845 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21846 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21847 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21848 // expression position calling the PG `left(string, n)` /
21849 // `right(string, n)` function; rebuild the AST as a regular
21850 // function call so the engine's apply_function dispatch picks
21851 // it up. Delegated to a #[inline(never)] helper so its locals
21852 // don't bloat this recursive `parse_atom` frame (the nesting
21853 // budget in `enter_nested` is tuned to parse_atom's size).
21854 Token::Left if matches!(self.peek(), Token::LParen) => {
21855 self.parse_lr_string_function_call("left")
21856 }
21857 Token::Right if matches!(self.peek(), Token::LParen) => {
21858 self.parse_lr_string_function_call("right")
21859 }
21860 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21861 // token; we match on the bare ident. NOT is a token
21862 // (consumed in the comparison rung), but `EXISTS (...)`
21863 // at the top of an expression starts here.
21864 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21865 self.parse_exists_atom(false)
21866 }
21867 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21868 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21869 // CASE is a bare ident; we dispatch on lowercase match.
21870 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21871 self.parse_case_atom()
21872 }
21873 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21874 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21875 // '…'`. Lower onto the ::cast node so the existing
21876 // runtime text→date/timestamp paths do the parsing. The
21877 // string must follow immediately, else the ident stays a
21878 // plain column reference.
21879 Token::Ident(s)
21880 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21881 && matches!(self.peek(), Token::String(_)) =>
21882 {
21883 let target =
21884 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21885 let Token::String(lit) = self.advance() else {
21886 unreachable!("peek guaranteed a string token");
21887 };
21888 Ok(Expr::Cast {
21889 expr: Box::new(Expr::Literal(Literal::String(lit))),
21890 target,
21891 })
21892 }
21893 // v7.39 (round 221) — the SQL-standard long spellings:
21894 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21895 // TIME ZONE '…'`. Consume the modifier and lower to the same
21896 // typed-literal cast (`timetz` / `timestamptz` for WITH).
21897 Token::Ident(s)
21898 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21899 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21900 || w.eq_ignore_ascii_case("without"))
21901 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21902 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21903 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21904 {
21905 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21906 self.advance(); // WITH / WITHOUT
21907 self.advance(); // TIME
21908 self.advance(); // ZONE
21909 let Token::String(lit) = self.advance() else {
21910 unreachable!("guard checked a string token");
21911 };
21912 let base = s.to_ascii_lowercase();
21913 let target = match (base.as_str(), with_tz) {
21914 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21915 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21916 (_, true) => CastTarget::Timestamptz,
21917 (_, false) => CastTarget::Timestamp,
21918 };
21919 Ok(Expr::Cast {
21920 expr: Box::new(Expr::Literal(Literal::String(lit))),
21921 target,
21922 })
21923 }
21924 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21925 // gathers the subquery's single-column rows (in its row order)
21926 // into an array. Desugared to `array_agg` over the subquery as a
21927 // derived table; out-of-line to keep parse_atom's frame small (it
21928 // sits on the recursive nesting-budget cycle).
21929 Token::Ident(s) | Token::QuotedIdent(s)
21930 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21931 {
21932 self.parse_array_subquery()
21933 }
21934 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21935 // is not a reserved token; we match by case-insensitive
21936 // ident. The opening `[` must follow immediately. v7.39 (read01
21937 // round 105) — the body moved out-of-line so its `Vec`/loop locals
21938 // leave parse_atom's frame (which sits on the nesting-budget cycle).
21939 Token::Ident(s) | Token::QuotedIdent(s)
21940 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21941 {
21942 self.parse_array_literal_body()
21943 }
21944 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21945 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21946 // We special-case before the generic ident dispatch so
21947 // the AGAINST clause never reaches the function-call
21948 // loop (which would mis-read `(cols) AGAINST` as a
21949 // call with no trailing modifier). The shape is
21950 // rewritten to a Boolean OR over per-column
21951 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21952 // term)` so the existing FTS evaluator handles
21953 // semantics — the fulltext-GIN built at CREATE TABLE
21954 // time is currently a "real index that survives dump
21955 // round-trip"; the planner hook that actually uses
21956 // it for posting-list intersection lands in a later
21957 // sub-phase (Phase 2.2b) without touching this surface.
21958 Token::Ident(s) | Token::QuotedIdent(s)
21959 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21960 {
21961 self.parse_match_against_atom()
21962 }
21963 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21964 // v7.37.43-T4 — PG-unreserved keywords are legal column /
21965 // alias names in expression context too. `release` appears
21966 // in sentori `0003_partition_events.sql` as both a column
21967 // reference (SELECT … release …) and an INSERT column list
21968 // entry. Mirrors `expect_ident_like`'s expansion of the
21969 // identifier set.
21970 other if unreserved_keyword_text(&other).is_some() => {
21971 let s = unreserved_keyword_text(&other).unwrap();
21972 self.finish_ident_atom(s)
21973 }
21974 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21975 // only inside `SET` before, so `SELECT @@autocommit` — which
21976 // every MySQL connector asks at handshake — was a parse error.
21977 // MariaDB accepts the bare, `@@session.` and `@@global.`
21978 // spellings alike and answers from the session's own value.
21979 Token::SessionVar(v) => {
21980 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21981 // has nothing to do with a `@@` engine setting: its own
21982 // per-session namespace, and an unset one reads NULL instead
21983 // of raising. Stripping every `@` (as this did) made `@x` and
21984 // `@@x` the same node, so `SELECT @x` answered "Unknown
21985 // system variable".
21986 Ok(variable_ref_atom(&v))
21987 }
21988 other => Err(ParseError {
21989 message: format!("unexpected token {other:?} in expression"),
21990 token_pos: tok_pos,
21991 }),
21992 }
21993 // After parsing the atom, fold any postfix `::vector` casts.
21994 .and_then(|atom| self.finish_postfix_casts(atom))
21995 }
21996
21997 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
21998 /// Both bind tighter than any binary op.
21999 /// Shared cast-target parser for postfix `::TYPE` and the
22000 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22001 /// If the next tokens are `( N )`, consume them and return the canonical
22002 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22003 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22004 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22005 if !matches!(self.peek(), Token::LParen) {
22006 return None;
22007 }
22008 self.advance(); // (
22009 let n = match self.advance() {
22010 Token::Integer(n) => n,
22011 _ => return Some(base.to_string()), // malformed → drop precision
22012 };
22013 if matches!(self.peek(), Token::RParen) {
22014 self.advance();
22015 }
22016 Some(alloc::format!("{base}({n})"))
22017 }
22018
22019 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22020 let target = match self.advance() {
22021 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22022 "int" | "integer" | "int4" => {
22023 if matches!(self.peek(), Token::LBracket)
22024 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22025 {
22026 self.advance();
22027 self.advance();
22028 CastTarget::IntArray
22029 } else {
22030 CastTarget::Int
22031 }
22032 }
22033 "bigint" | "int8" => {
22034 if matches!(self.peek(), Token::LBracket)
22035 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22036 {
22037 self.advance();
22038 self.advance();
22039 CastTarget::BigIntArray
22040 } else {
22041 CastTarget::BigInt
22042 }
22043 }
22044 "float" | "double" => CastTarget::Float,
22045 "text" => {
22046 // v7.10.11 — `::TEXT[]` widens to TextArray.
22047 if matches!(self.peek(), Token::LBracket)
22048 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22049 {
22050 self.advance();
22051 self.advance();
22052 CastTarget::TextArray
22053 } else {
22054 CastTarget::Text
22055 }
22056 }
22057 "bool" | "boolean" => CastTarget::Bool,
22058 "vector" => CastTarget::Vector,
22059 "date" => CastTarget::Date,
22060 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22061 // seconds precision through the Named path (the engine rounds
22062 // the sub-second field); bare `::timestamp` keeps the fast arm.
22063 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22064 Some(named) => CastTarget::Named(named),
22065 None => CastTarget::Timestamp,
22066 },
22067 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22068 Some(named) => CastTarget::Named(named),
22069 None => CastTarget::Timestamptz,
22070 },
22071 "interval" => CastTarget::Interval,
22072 "json" => CastTarget::Json,
22073 "jsonb" => CastTarget::Jsonb,
22074 // v7.39 (round 694) — these have dedicated CastTarget
22075 // variants, so they never reached the postfix `[]` handling
22076 // further down and `::regtype[]` was a SYNTAX error at the
22077 // `]`. PG has an array type for every scalar; take the
22078 // suffix here and hand the canonical `<ty>_array` name to
22079 // the engine, the same shape every other array cast uses.
22080 "regtype" if self.peek_postfix_array_brackets() => {
22081 self.advance();
22082 self.advance();
22083 CastTarget::Named(alloc::string::String::from("regtype_array"))
22084 }
22085 "regclass" if self.peek_postfix_array_brackets() => {
22086 self.advance();
22087 self.advance();
22088 CastTarget::Named(alloc::string::String::from("regclass_array"))
22089 }
22090 "regtype" => CastTarget::RegType,
22091 "regclass" => CastTarget::RegClass,
22092 // v7.12.0 — `::tsvector` / `::tsquery`.
22093 // Engine decodes the LHS text via the PG
22094 // external form parser.
22095 // v7.39 (round 352, M8) — MySQL's own cast targets.
22096 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22097 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22098 // such type, so they are taken only in that dialect and
22099 // fall through to the "type does not exist" arm otherwise.
22100 "signed" | "unsigned" if self.mysql_dialect => {
22101 if matches!(self.peek(), Token::Ident(k)
22102 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22103 {
22104 self.advance();
22105 }
22106 CastTarget::Named(s.to_ascii_lowercase())
22107 }
22108 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22109 // in MySQL: MariaDB answers '123' where the SQL-standard
22110 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22111 // Truncating a number to its first digit is a wrong answer
22112 // with no error, so the MySQL session gets MySQL's reading.
22113 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22114 CastTarget::Text
22115 }
22116 "tsvector" => CastTarget::TsVector,
22117 "tsquery" => CastTarget::TsQuery,
22118 // v7.17.0 — `::uuid`. Engine decodes the LHS
22119 // text via `spg_storage::parse_uuid_str`.
22120 "uuid" => CastTarget::Uuid,
22121 // v7.18 — `::bytea`. Engine decodes the LHS
22122 // text via the PG hex form (`'\xdeadbeef'`)
22123 // or escape form (`'\\x05\\x00'`). Closes
22124 // mailrs D-pre #3 reverse-acceptance gap.
22125 "bytea" => CastTarget::Bytea,
22126 // v7.37.5 ship triage — generic typed-cast escape.
22127 // Anything the long-tail PG type ident table knows
22128 // about(network/bit/geometry/multirange/etc.)flows
22129 // through `CastTarget::Named(canonical)`; the engine
22130 // resolves via `column_type_to_data_type` and dispatches
22131 // through the typed `coerce_value` path. Truly
22132 // unrecognised idents still hit the error arm below
22133 // because the engine rejects them.
22134 other => {
22135 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22136 // `::varchar(255)`, etc. Capture into the canonical
22137 // `name(p,s)` form so `type_name_to_data_type` can
22138 // reconstruct the `DataType::Numeric { precision,
22139 // scale }` (and similar param-carrying types).
22140 let mut name = other.to_string();
22141 // v7.39 (round 281) — `::bit varying(3)` is two
22142 // words; fold the tail in so the typmod reaches the
22143 // type resolver instead of tripping the parser.
22144 if name.eq_ignore_ascii_case("bit")
22145 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22146 {
22147 self.advance();
22148 name = alloc::string::String::from("varbit");
22149 }
22150 // v7.39 (round 613) — `::character varying` is the same
22151 // two-word shape and had no fold, so the `varying` was
22152 // left behind and the cast became a bare `character`,
22153 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22154 // `a` where PG answers `ab`. Silently, and for a spelling
22155 // pg_dump writes.
22156 if name.eq_ignore_ascii_case("character")
22157 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22158 {
22159 self.advance();
22160 name = alloc::string::String::from("varchar");
22161 }
22162 if matches!(self.peek(), Token::LParen) {
22163 let mut buf = alloc::string::String::from("(");
22164 let mut depth = 0usize;
22165 loop {
22166 match self.advance() {
22167 Token::LParen => {
22168 depth += 1;
22169 if depth > 1 {
22170 buf.push('(');
22171 }
22172 }
22173 Token::RParen => {
22174 depth -= 1;
22175 if depth == 0 {
22176 buf.push(')');
22177 break;
22178 }
22179 buf.push(')');
22180 }
22181 Token::Comma => buf.push(','),
22182 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22183 // v7.39 (round 273) — a minus used to fall
22184 // into the catch-all below and vanish, so
22185 // `::numeric(10,-2)` reached the engine as
22186 // the text `numeric(10,2)` and silently
22187 // rounded to two DECIMALS instead of to
22188 // hundreds. A dropped token is not a
22189 // no-op when it carries a sign.
22190 Token::Minus => buf.push('-'),
22191 Token::Eof => break,
22192 _ => {}
22193 }
22194 }
22195 name.push_str(&buf);
22196 }
22197 // Optional postfix `[]` widens to the array form —
22198 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22199 // The engine's `type_name_to_data_type` recognises
22200 // the canonical `<ty>_array` form.
22201 if matches!(self.peek(), Token::LBracket)
22202 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22203 {
22204 self.advance();
22205 self.advance();
22206 name.push_str("_array");
22207 }
22208 CastTarget::Named(name)
22209 }
22210 },
22211 Token::Interval => CastTarget::Interval,
22212 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22213 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22214 // = char(1)); other quoted names resolve like idents.
22215 Token::QuotedIdent(q) => {
22216 if q.eq_ignore_ascii_case("char") {
22217 CastTarget::Named("char1".into())
22218 } else {
22219 CastTarget::Named(q.to_ascii_lowercase())
22220 }
22221 }
22222 other => {
22223 return Err(ParseError {
22224 message: format!("expected type ident after `::`, got {other:?}"),
22225 token_pos: self.consumed_pos(),
22226 });
22227 }
22228 };
22229 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22230 // target to its array sibling. Closed-enum arms (Bool /
22231 // SmallInt / Numeric / Float / Date / …) didn't carry the
22232 // explicit widening that Text / Int / BigInt did, so
22233 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22234 // error. The widening here mirrors the per-arm Text /
22235 // Int / BigInt logic above + folds the new ζ-A first-class
22236 // types through `CastTarget::Named("<ty>_array")`.
22237 if matches!(self.peek(), Token::LBracket)
22238 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22239 {
22240 let widened = match &target {
22241 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22242 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22243 // v7.39 (round 326, V43) — the two temporal types stay
22244 // distinct. Both used to widen to `timestamptz_array`, so
22245 // `::timestamp[]` named the wrong target in its own error
22246 // message and lost the zone-less identity on the way.
22247 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22248 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22249 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22250 CastTarget::Json | CastTarget::Jsonb => {
22251 Some(CastTarget::Named("jsonb_array".to_string()))
22252 }
22253 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22254 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22255 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22256 CastTarget::Named(name) => {
22257 let mut a = name.clone();
22258 a.push_str("_array");
22259 Some(CastTarget::Named(a))
22260 }
22261 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22262 // RegType / RegClass / TextArray / IntArray /
22263 // BigIntArray already finalised — leave as is.
22264 _ => None,
22265 };
22266 if let Some(w) = widened {
22267 self.advance();
22268 self.advance();
22269 return Ok(w);
22270 }
22271 }
22272 Ok(target)
22273 }
22274
22275 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22276 loop {
22277 // v7.38 (read01, T9) — composite field access `(expr).field`.
22278 // A bare `a.b` is consumed as a qualified column inside the ident
22279 // atom, so a Dot only survives to this postfix position when the
22280 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22281 // `.*` whole-row expansion is not handled here (projection-level).
22282 if matches!(self.peek(), Token::Dot)
22283 && matches!(
22284 self.tokens.get(self.pos + 1),
22285 Some(Token::Ident(_) | Token::QuotedIdent(_))
22286 )
22287 {
22288 self.advance(); // .
22289 let field = match self.advance() {
22290 Token::Ident(s) | Token::QuotedIdent(s) => s,
22291 other => {
22292 return Err(
22293 self.err(format!("expected a field name after '.', got {other:?}"))
22294 );
22295 }
22296 };
22297 expr = Expr::FieldAccess {
22298 base: Box::new(expr),
22299 field,
22300 };
22301 continue;
22302 }
22303 if matches!(self.peek(), Token::DoubleColon) {
22304 self.advance();
22305 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22306 // target set to include INTERVAL (reserved Token),
22307 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22308 // mailrs follow-up H3a + H3b.
22309 let target = self.parse_cast_target()?;
22310 expr = Expr::Cast {
22311 expr: Box::new(expr),
22312 target,
22313 };
22314 continue;
22315 }
22316 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22317 // returns NULL for out-of-range. Multiple subscripts
22318 // chain: `a[i][j]` parses left-to-right.
22319 if matches!(self.peek(), Token::LBracket) {
22320 self.advance();
22321 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22322 // bare index stays a subscript.
22323 let lo = if matches!(self.peek(), Token::Colon) {
22324 None
22325 } else {
22326 Some(self.parse_expr(0)?)
22327 };
22328 if matches!(self.peek(), Token::Colon) {
22329 self.advance();
22330 let hi = if matches!(self.peek(), Token::RBracket) {
22331 None
22332 } else {
22333 Some(Box::new(self.parse_expr(0)?))
22334 };
22335 if !matches!(self.peek(), Token::RBracket) {
22336 return Err(self.err(alloc::format!(
22337 "expected ']' after array slice, got {:?}",
22338 self.peek()
22339 )));
22340 }
22341 self.advance();
22342 expr = Expr::ArraySlice {
22343 target: Box::new(expr),
22344 lo: lo.map(Box::new),
22345 hi,
22346 };
22347 continue;
22348 }
22349 let index = lo.expect("non-colon branch parsed an index");
22350 if !matches!(self.peek(), Token::RBracket) {
22351 return Err(self.err(alloc::format!(
22352 "expected ']' after array index, got {:?}",
22353 self.peek()
22354 )));
22355 }
22356 self.advance();
22357 expr = Expr::ArraySubscript {
22358 target: Box::new(expr),
22359 index: Box::new(index),
22360 };
22361 continue;
22362 }
22363 // `expr AT TIME ZONE zone` — lowers to PG's own function
22364 // form timezone(zone, expr); the scalar implements the
22365 // offset shift (named zones error there — no tzdata).
22366 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22367 && matches!(self.tokens.get(self.pos + 1),
22368 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22369 && matches!(self.tokens.get(self.pos + 2),
22370 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22371 {
22372 self.advance(); // AT
22373 self.advance(); // TIME
22374 self.advance(); // ZONE
22375 // Zone at comparison precedence so AND/OR stay out.
22376 let zone = self.parse_expr(6)?;
22377 expr = Expr::FunctionCall {
22378 name: "timezone".to_string(),
22379 args: alloc::vec![zone, expr],
22380 };
22381 continue;
22382 }
22383 // `expr COLLATE "name"` — SPG's single text ordering IS
22384 // byte order, i.e. the C collation. The byte-order
22385 // spellings absorb as no-ops; a locale collation would
22386 // silently sort differently from PG, so it errors
22387 // honestly instead.
22388 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22389 self.advance();
22390 let mut cname = match self.advance() {
22391 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22392 other => {
22393 return Err(self.err(alloc::format!(
22394 "expected collation name after COLLATE, got {other:?}"
22395 )));
22396 }
22397 };
22398 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22399 // is how `pg_dump` writes the default one:
22400 // `… COLLATE pg_catalog.default`. Reading a single token
22401 // left the SCHEMA as the name, so the clause was refused
22402 // as an unsupported locale collation and no dump ran.
22403 if matches!(self.peek(), Token::Dot) {
22404 self.advance();
22405 cname = match self.advance() {
22406 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22407 // `default` lexes as a KEYWORD, and it is the name
22408 // pg_dump writes — the same trap round 535 hit with
22409 // TABLE / INDEX / FULL.
22410 Token::Default => alloc::string::String::from("default"),
22411 other => {
22412 return Err(self.err(alloc::format!(
22413 "expected collation name after COLLATE, got {other:?}"
22414 )));
22415 }
22416 };
22417 }
22418 let lc = cname.to_ascii_lowercase();
22419 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22420 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22421 // family / `binary`) forces byte-wise, which is exactly
22422 // what `BINARY expr` does — lower onto that so every fold
22423 // site (comparison, LIKE, ORDER BY) suppresses via
22424 // `is_binary_coerced`. A `_ci` family override folds, and
22425 // under the MySQL dialect the default already folds, so it
22426 // absorbs as a no-op; likewise the C / byte-order spellings.
22427 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22428 expr = Expr::Cast {
22429 expr: alloc::boxed::Box::new(expr),
22430 target: CastTarget::Named("binary".to_string()),
22431 };
22432 continue;
22433 }
22434 let mysql_ci = self.mysql_dialect
22435 && (lc.ends_with("_ci")
22436 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22437 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22438 // goes to the lowering channel, the byte-order spellings
22439 // included. Round 691 recorded only the names the old
22440 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22441 // absorbed as a no-op — and once a column could declare a
22442 // collation, absorbing the clause meant the COLUMN's
22443 // collation won where the query had asked for bytes.
22444 if self.in_order_by_key && !mysql_ci {
22445 self.order_key_collation = Some(cname);
22446 continue;
22447 }
22448 if !matches!(
22449 lc.as_str(),
22450 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22451 ) && !mysql_ci
22452 {
22453 return Err(self.err(alloc::format!(
22454 "COLLATE {cname:?}: SPG orders text by bytes (the C \
22455 collation); locale collations are not supported yet — \
22456 use COLLATE \"C\" or drop the clause"
22457 )));
22458 }
22459 continue;
22460 }
22461 return Ok(expr);
22462 }
22463 }
22464
22465 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22466 /// the first token that is not one. Schema qualifiers collapse to the
22467 /// last part, which is what every other name path here does (SPG is
22468 /// single-schema).
22469 fn take_comma_separated_names(&mut self) -> Vec<String> {
22470 let mut out = Vec::new();
22471 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22472 self.advance();
22473 let mut last = n;
22474 while matches!(self.peek(), Token::Dot) {
22475 self.advance();
22476 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22477 last = t;
22478 }
22479 }
22480 out.push(last);
22481 if matches!(self.peek(), Token::Comma) {
22482 self.advance();
22483 } else {
22484 break;
22485 }
22486 }
22487 out
22488 }
22489
22490 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22491 ///
22492 /// The general cast-target path tests this inline; the types with their
22493 /// own `CastTarget` variant need it as a guard on their match arm,
22494 /// which is what this exists for.
22495 fn peek_postfix_array_brackets(&self) -> bool {
22496 matches!(self.peek(), Token::LBracket)
22497 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22498 }
22499
22500 /// Parse the operator tail after a `(a, b, …)` row constructor
22501 /// and expand at parse time. `=` is the conjunction of element
22502 /// equalities; `<>` its negation; the order operators expand
22503 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22504 /// equalities. Anything else (a bare row value, a subquery
22505 /// RHS) errors honestly — SPG has no composite runtime value.
22506 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22507 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22508 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22509 lhs: Box::new(l.clone()),
22510 op: BinOp::Eq,
22511 rhs: Box::new(r.clone()),
22512 });
22513 let first = it.next().expect("row has at least two elements");
22514 it.fold(first, |acc, e| Expr::Binary {
22515 lhs: Box::new(acc),
22516 op: BinOp::And,
22517 rhs: Box::new(e),
22518 })
22519 }
22520 // Lexicographic (a,b) OP (c,d):
22521 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22522 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22523 if lhs.len() == 1 {
22524 return Expr::Binary {
22525 lhs: Box::new(lhs[0].clone()),
22526 op: last,
22527 rhs: Box::new(rhs[0].clone()),
22528 };
22529 }
22530 let head_strict = Expr::Binary {
22531 lhs: Box::new(lhs[0].clone()),
22532 op: strict,
22533 rhs: Box::new(rhs[0].clone()),
22534 };
22535 let head_eq = Expr::Binary {
22536 lhs: Box::new(lhs[0].clone()),
22537 op: BinOp::Eq,
22538 rhs: Box::new(rhs[0].clone()),
22539 };
22540 Expr::Binary {
22541 lhs: Box::new(head_strict),
22542 op: BinOp::Or,
22543 rhs: Box::new(Expr::Binary {
22544 lhs: Box::new(head_eq),
22545 op: BinOp::And,
22546 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22547 }),
22548 }
22549 }
22550 let negated_in = if matches!(self.peek(), Token::Not)
22551 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22552 {
22553 self.advance();
22554 true
22555 } else {
22556 false
22557 };
22558 if matches!(self.peek(), Token::In) {
22559 self.advance();
22560 if !matches!(self.peek(), Token::LParen) {
22561 return Err(self.err(alloc::format!(
22562 "expected '(' after row IN, got {:?}",
22563 self.peek()
22564 )));
22565 }
22566 self.advance();
22567 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22568 // not a list of literal rows. Row-vs-list decomposes to
22569 // OR-of-AND above, but the subquery's rows are only known at
22570 // runtime, so keep it as a RowInSubquery node.
22571 if matches!(self.peek(), Token::Select) {
22572 let inner = self.parse_select_stmt()?;
22573 if !matches!(self.peek(), Token::RParen) {
22574 return Err(self.err(alloc::format!(
22575 "expected ')' after row IN-subquery, got {:?}",
22576 self.peek()
22577 )));
22578 }
22579 self.advance();
22580 let Statement::Select(s) = inner else {
22581 unreachable!("parse_select_stmt always returns Statement::Select")
22582 };
22583 return Ok(Expr::RowInSubquery {
22584 row,
22585 subquery: Box::new(s),
22586 negated: negated_in,
22587 });
22588 }
22589 let mut alternatives: Vec<Expr> = Vec::new();
22590 loop {
22591 // Optional ROW keyword before the paren row.
22592 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22593 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22594 {
22595 self.advance();
22596 }
22597 if !matches!(self.peek(), Token::LParen) {
22598 return Err(self.err(alloc::format!(
22599 "expected '(' to open a row inside IN, got {:?}",
22600 self.peek()
22601 )));
22602 }
22603 self.advance();
22604 let mut rhs = alloc::vec![self.parse_expr(0)?];
22605 while matches!(self.peek(), Token::Comma) {
22606 self.advance();
22607 rhs.push(self.parse_expr(0)?);
22608 }
22609 if !matches!(self.peek(), Token::RParen) {
22610 return Err(self.err(alloc::format!(
22611 "expected ')' after row inside IN, got {:?}",
22612 self.peek()
22613 )));
22614 }
22615 self.advance();
22616 if rhs.len() != row.len() {
22617 return Err(self.err(alloc::format!(
22618 "row IN arity mismatch: left has {}, right has {}",
22619 row.len(),
22620 rhs.len()
22621 )));
22622 }
22623 alternatives.push(row_eq(&row, &rhs));
22624 if matches!(self.peek(), Token::Comma) {
22625 self.advance();
22626 continue;
22627 }
22628 break;
22629 }
22630 if !matches!(self.peek(), Token::RParen) {
22631 return Err(self.err(alloc::format!(
22632 "expected ')' to close row IN list, got {:?}",
22633 self.peek()
22634 )));
22635 }
22636 self.advance();
22637 let mut it = alternatives.into_iter();
22638 let first = it.next().expect("IN list has at least one row");
22639 let combined = it.fold(first, |acc, e| Expr::Binary {
22640 lhs: Box::new(acc),
22641 op: BinOp::Or,
22642 rhs: Box::new(e),
22643 });
22644 return Ok(maybe_not(combined, negated_in));
22645 }
22646 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22647 // two periods share at least one time point. Each pair is
22648 // normalised with least/greatest (PG accepts the endpoints
22649 // in either order), then lowered to the standard
22650 // `start1 < end2 AND start2 < end1` form.
22651 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22652 if row.len() != 2 {
22653 return Err(self.err(alloc::format!(
22654 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22655 row.len()
22656 )));
22657 }
22658 self.advance();
22659 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22660 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22661 {
22662 self.advance();
22663 }
22664 if !matches!(self.peek(), Token::LParen) {
22665 return Err(self.err(alloc::format!(
22666 "expected '(' after OVERLAPS, got {:?}",
22667 self.peek()
22668 )));
22669 }
22670 self.advance();
22671 let r0 = self.parse_expr(0)?;
22672 if !matches!(self.peek(), Token::Comma) {
22673 return Err(self.err(alloc::format!(
22674 "OVERLAPS needs (start, end) on the right, got {:?}",
22675 self.peek()
22676 )));
22677 }
22678 self.advance();
22679 let r1 = self.parse_expr(0)?;
22680 if !matches!(self.peek(), Token::RParen) {
22681 return Err(self.err(alloc::format!(
22682 "expected ')' after OVERLAPS pair, got {:?}",
22683 self.peek()
22684 )));
22685 }
22686 self.advance();
22687 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22688 name: String::from(name),
22689 args: alloc::vec![a.clone(), b.clone()],
22690 };
22691 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22692 lhs: Box::new(lhs),
22693 op: BinOp::Lt,
22694 rhs: Box::new(rhs),
22695 };
22696 return Ok(Expr::Binary {
22697 lhs: Box::new(lt(
22698 pair_fn("least", &row[0], &row[1]),
22699 pair_fn("greatest", &r0, &r1),
22700 )),
22701 op: BinOp::And,
22702 rhs: Box::new(lt(
22703 pair_fn("least", &r0, &r1),
22704 pair_fn("greatest", &row[0], &row[1]),
22705 )),
22706 });
22707 }
22708 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22709 // PG, `IS NULL` is true only when EVERY field is NULL, and
22710 // `IS NOT NULL` is true only when every field is non-NULL — the
22711 // latter is NOT the negation of the former (a mixed row is
22712 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22713 // which reproduces exactly that all-fields semantics.
22714 if matches!(self.peek(), Token::Is) {
22715 self.advance();
22716 let negated = if matches!(self.peek(), Token::Not) {
22717 self.advance();
22718 true
22719 } else {
22720 false
22721 };
22722 if !matches!(self.peek(), Token::Null) {
22723 return Err(self.err(alloc::format!(
22724 "expected NULL after row IS [NOT], got {:?}",
22725 self.peek()
22726 )));
22727 }
22728 self.advance();
22729 let mut it = row.iter().map(|e| Expr::IsNull {
22730 expr: Box::new(e.clone()),
22731 negated,
22732 });
22733 let first = it.next().expect("row has at least two elements");
22734 return Ok(it.fold(first, |acc, e| Expr::Binary {
22735 lhs: Box::new(acc),
22736 op: BinOp::And,
22737 rhs: Box::new(e),
22738 }));
22739 }
22740 let op = match self.peek() {
22741 Token::Eq => BinOp::Eq,
22742 Token::NotEq => BinOp::NotEq,
22743 Token::Lt => BinOp::Lt,
22744 Token::LtEq => BinOp::LtEq,
22745 Token::Gt => BinOp::Gt,
22746 Token::GtEq => BinOp::GtEq,
22747 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22748 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22749 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22750 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22751 // (`::text`, `.field`) applies at the caller just as it does for the
22752 // ROW(...) node. All the comparison / predicate forms returned above.
22753 _ => {
22754 return Ok(Expr::FunctionCall {
22755 name: String::from("row"),
22756 args: row,
22757 });
22758 }
22759 };
22760 self.advance();
22761 // Optional ROW keyword before the paren row.
22762 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22763 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22764 {
22765 self.advance();
22766 }
22767 if !matches!(self.peek(), Token::LParen) {
22768 return Err(self.err(alloc::format!(
22769 "expected '(' to open the right-hand row, got {:?}",
22770 self.peek()
22771 )));
22772 }
22773 self.advance();
22774 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22775 // subquery. Kept as a node (the subquery's row is a runtime value);
22776 // the literal-RHS form below still decomposes at parse time.
22777 if matches!(self.peek(), Token::Select) {
22778 let inner = self.parse_select_stmt()?;
22779 if !matches!(self.peek(), Token::RParen) {
22780 return Err(self.err(alloc::format!(
22781 "expected ')' after row comparison subquery, got {:?}",
22782 self.peek()
22783 )));
22784 }
22785 self.advance();
22786 let Statement::Select(s) = inner else {
22787 unreachable!("parse_select_stmt always returns Statement::Select")
22788 };
22789 return Ok(Expr::RowCmpSubquery {
22790 row,
22791 op,
22792 subquery: Box::new(s),
22793 });
22794 }
22795 let mut rhs = alloc::vec![self.parse_expr(0)?];
22796 while matches!(self.peek(), Token::Comma) {
22797 self.advance();
22798 rhs.push(self.parse_expr(0)?);
22799 }
22800 if !matches!(self.peek(), Token::RParen) {
22801 return Err(self.err(alloc::format!(
22802 "expected ')' after right-hand row, got {:?}",
22803 self.peek()
22804 )));
22805 }
22806 self.advance();
22807 if rhs.len() != row.len() {
22808 // v7.39 (round 239) — PG's wording (42601).
22809 return Err(self.err("unequal number of entries in row expressions".to_string()));
22810 }
22811 Ok(match op {
22812 BinOp::Eq => row_eq(&row, &rhs),
22813 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22814 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22815 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22816 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22817 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22818 _ => unreachable!("op restricted above"),
22819 })
22820 }
22821
22822 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22823 /// escape character becomes the matcher's default backslash:
22824 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22825 /// → the char itself, and any pre-existing backslash escapes
22826 /// itself so it stays literal. Both operands must be string
22827 /// literals — a runtime pattern would need matcher support.
22828 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22829 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22830 (&pattern, &esc)
22831 else {
22832 return Err(
22833 "LIKE ... ESCAPE requires string-literal pattern and escape \
22834 (runtime escape characters are not supported yet)"
22835 .into(),
22836 );
22837 };
22838 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22839 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22840 // multi-character escape is an error.
22841 let esc_ch: Option<char> = {
22842 let mut ch_iter = e.chars();
22843 match (ch_iter.next(), ch_iter.next()) {
22844 (Some(c), None) => Some(c),
22845 (None, _) => None,
22846 (Some(_), Some(_)) => {
22847 return Err(alloc::format!(
22848 "ESCAPE must be a single character, got {e:?}"
22849 ));
22850 }
22851 }
22852 };
22853 let mut out = String::with_capacity(p.len() + 4);
22854 let mut chars = p.chars();
22855 while let Some(c) = chars.next() {
22856 if Some(c) == esc_ch {
22857 match chars.next() {
22858 // Escaped wildcard / escaped escape → keep the
22859 // next char literal via backslash.
22860 Some(next) => {
22861 out.push('\\');
22862 out.push(next);
22863 }
22864 None => {
22865 return Err("LIKE pattern ends with the escape character".into());
22866 }
22867 }
22868 } else if c == '\\' && esc_ch != Some('\\') {
22869 // A raw backslash is literal under a custom (or absent) escape
22870 // — escape it for the backslash-based matcher.
22871 out.push('\\');
22872 out.push('\\');
22873 } else {
22874 out.push(c);
22875 }
22876 }
22877 Ok(Expr::Literal(Literal::String(out)))
22878 }
22879
22880 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22881 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22882 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22883 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22884 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22885 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22886 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22887 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22888 /// array expression errors honestly rather than silently mismatching.
22889 fn try_like_any_all(
22890 &mut self,
22891 base: &Expr,
22892 negated: bool,
22893 case_insensitive: bool,
22894 ) -> Result<Option<Expr>, ParseError> {
22895 let is_any = match self.peek() {
22896 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22897 Token::Ident(s)
22898 if s.eq_ignore_ascii_case("any")
22899 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22900 {
22901 true
22902 }
22903 _ => return Ok(None),
22904 };
22905 self.advance(); // ANY / ALL
22906 self.advance(); // '('
22907 let arr = self.parse_expr(0)?;
22908 if !matches!(self.peek(), Token::RParen) {
22909 return Err(self.err(format!(
22910 "expected ')' after LIKE {} argument, got {:?}",
22911 if is_any { "ANY" } else { "ALL" },
22912 self.peek()
22913 )));
22914 }
22915 self.advance(); // ')'
22916 let Expr::Array(items) = arr else {
22917 return Err(self.err(
22918 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22919 ));
22920 };
22921 let mut clauses = items.into_iter().map(|p| Expr::Like {
22922 expr: Box::new(base.clone()),
22923 pattern: Box::new(p),
22924 negated,
22925 case_insensitive,
22926 });
22927 let Some(first) = clauses.next() else {
22928 // ANY(empty) = FALSE, ALL(empty) = TRUE.
22929 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22930 };
22931 let op = if is_any { BinOp::Or } else { BinOp::And };
22932 let combined = clauses.fold(first, |acc, c| Expr::Binary {
22933 lhs: Box::new(acc),
22934 op,
22935 rhs: Box::new(c),
22936 });
22937 Ok(Some(combined))
22938 }
22939
22940 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
22941 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22942 /// `AND` is not swallowed.
22943 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22944 self.advance(); // BETWEEN
22945 // SYMMETRIC — the bounds may arrive in either order; both
22946 // orientations OR together. ASYMMETRIC is the default and
22947 // absorbs as noise.
22948 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22949 {
22950 self.advance();
22951 true
22952 } else {
22953 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22954 self.advance();
22955 }
22956 false
22957 };
22958 let low = self.parse_expr(6)?;
22959 if !matches!(self.peek(), Token::And) {
22960 return Err(self.err(format!(
22961 "expected AND after BETWEEN low bound, got {:?}",
22962 self.peek()
22963 )));
22964 }
22965 self.advance();
22966 let high = self.parse_expr(6)?;
22967 let target = Box::new(expr);
22968 let range = |lo: Expr, hi: Expr| Expr::Binary {
22969 lhs: Box::new(Expr::Binary {
22970 lhs: target.clone(),
22971 op: BinOp::GtEq,
22972 rhs: Box::new(lo),
22973 }),
22974 op: BinOp::And,
22975 rhs: Box::new(Expr::Binary {
22976 lhs: target.clone(),
22977 op: BinOp::LtEq,
22978 rhs: Box::new(hi),
22979 }),
22980 };
22981 let combined = if symmetric {
22982 Expr::Binary {
22983 lhs: Box::new(range(low.clone(), high.clone())),
22984 op: BinOp::Or,
22985 rhs: Box::new(range(high, low)),
22986 }
22987 } else {
22988 range(low, high)
22989 };
22990 Ok(maybe_not(combined, negated))
22991 }
22992
22993 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
22994 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
22995 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
22996 /// Caller already consumed the leading `WITH` ident.
22997 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
22998 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
22999 /// self-reference that appears more than once in a single term.
23000 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23001 use crate::ast::{CteBody, SelectStatement};
23002 if !cte.recursive {
23003 return Ok(());
23004 }
23005 let CteBody::Select(body) = &cte.body else {
23006 return Ok(());
23007 };
23008 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23009 // check the anchor and every peer term.
23010 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23011 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23012 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23013 return Err(self.err(String::from(
23014 "ORDER BY in a recursive query is not implemented",
23015 )));
23016 }
23017 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23018 return Err(self.err(String::from(
23019 "LIMIT in a recursive query is not implemented",
23020 )));
23021 }
23022 let self_refs = |s: &SelectStatement| -> usize {
23023 let Some(from) = &s.from else {
23024 return 0;
23025 };
23026 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23027 for j in &from.joins {
23028 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23029 n += 1;
23030 }
23031 }
23032 n
23033 };
23034 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23035 return Err(self.err(alloc::format!(
23036 "recursive reference to query \"{}\" must not appear more than once",
23037 cte.name
23038 )));
23039 }
23040 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23041 // apply only when the body actually references itself (a non-self-
23042 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23043 let anchor_refs = self_refs(body);
23044 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23045 if anchor_refs > 0 || union_refs {
23046 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23047 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23048 // "does not have the form" error — SPG used to compute a value.
23049 if body.unions.is_empty()
23050 || body.unions.iter().any(|(k, _)| {
23051 !matches!(
23052 k,
23053 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23054 )
23055 })
23056 {
23057 return Err(self.err(alloc::format!(
23058 "recursive query \"{}\" does not have the form non-recursive-term \
23059 UNION [ALL] recursive-term",
23060 cte.name
23061 )));
23062 }
23063 if anchor_refs > 0 {
23064 return Err(self.err(alloc::format!(
23065 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23066 cte.name
23067 )));
23068 }
23069 }
23070 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23071 for (_, u) in &body.unions {
23072 if self_refs(u) == 0 {
23073 continue;
23074 }
23075 // The self-reference must not sit on the nullable side of an outer
23076 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23077 if let Some(from) = &u.from {
23078 for (i, j) in from.joins.iter().enumerate() {
23079 let left_has_self = is_self(&from.primary)
23080 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23081 let violated = match j.kind {
23082 crate::ast::JoinKind::Left => is_self(&j.table),
23083 crate::ast::JoinKind::Right => left_has_self,
23084 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23085 _ => false,
23086 };
23087 if violated {
23088 return Err(self.err(alloc::format!(
23089 "recursive reference to query \"{}\" must not appear within an outer join",
23090 cte.name
23091 )));
23092 }
23093 }
23094 }
23095 // No aggregates at the top level of the recursive term (SPG used
23096 // to run them and surface a misleading downstream error).
23097 let mut items_and_having: Vec<&Expr> = Vec::new();
23098 for it in &u.items {
23099 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23100 items_and_having.push(expr);
23101 }
23102 }
23103 if let Some(h) = &u.having {
23104 items_and_having.push(h);
23105 }
23106 for e in items_and_having {
23107 if expr_has_toplevel_aggregate(e) {
23108 return Err(self.err(String::from(
23109 "aggregate functions are not allowed in a recursive query's recursive term",
23110 )));
23111 }
23112 }
23113 }
23114 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23115 // subquery) anywhere in the body is rejected; a plain FROM derived
23116 // table is legal in PG and untouched here.
23117 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23118 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23119 for term in all_terms {
23120 if select_has_self_ref_in_sublink(term, &cte.name) {
23121 return Err(self.err(alloc::format!(
23122 "recursive reference to query \"{}\" must not appear within a subquery",
23123 cte.name
23124 )));
23125 }
23126 }
23127 Ok(())
23128 }
23129
23130 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23131 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23132 /// right after parse so the engine sees a plain recursive CTE with the
23133 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23134 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23135 /// text-rendered rows can't provide, and errors honestly.
23136 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23137 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23138 if cte.search.is_none() && cte.cycle.is_none() {
23139 return Ok(());
23140 }
23141 let cte_name = cte.name.clone();
23142 let col_names = cte.column_overrides.clone();
23143 if col_names.is_empty() {
23144 return Err(
23145 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23146 );
23147 }
23148 let search = cte.search.take();
23149 let cycle = cte.cycle.take();
23150 let mut extra_cols: Vec<String> = Vec::new();
23151 let col_ref = |name: &str| {
23152 Expr::Column(ColumnName {
23153 qualifier: Some(cte_name.clone()),
23154 name: name.to_string(),
23155 })
23156 };
23157 // Position of a SEARCH/CYCLE column within the CTE's column list.
23158 let pos_of = |name: &str| -> Result<usize, ParseError> {
23159 col_names
23160 .iter()
23161 .position(|c| c.eq_ignore_ascii_case(name))
23162 .ok_or_else(|| {
23163 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23164 })
23165 };
23166 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23167 let mut args = Vec::with_capacity(positions.len());
23168 for &p in positions {
23169 match items.get(p) {
23170 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23171 _ => {
23172 return Err(self.err(
23173 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23174 ));
23175 }
23176 }
23177 }
23178 Ok(Expr::FunctionCall {
23179 name: "row".into(),
23180 args,
23181 })
23182 };
23183 let CteBody::Select(body) = &mut cte.body else {
23184 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23185 };
23186 if body.unions.is_empty() {
23187 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23188 }
23189 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23190
23191 if let Some(srch) = search {
23192 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23193 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23194 // no typed `record[]`, but element-wise array ORDER BY is correct
23195 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23196 // exactly onto a typed array: DEPTH is the root→node path
23197 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23198 // orders numerically (multi-digit keys included), matching PG.
23199 //
23200 // A multi-column BY would need a record[] to keep the per-node key
23201 // tuple orderable, which SPG can't express — error honestly there
23202 // rather than mis-order.
23203 if srch.by_columns.len() != 1 {
23204 return Err(self.err(
23205 "SEARCH … BY with multiple columns needs typed record[] ordering \
23206 SPG doesn't have yet; a single BY column is supported"
23207 .into(),
23208 ));
23209 }
23210 let key_pos = pos_of(&srch.by_columns[0])?;
23211 let base_key = match body.items.get(key_pos) {
23212 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23213 _ => {
23214 return Err(
23215 self.err("SEARCH BY column maps to a non-expression select item".into())
23216 );
23217 }
23218 };
23219 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23220 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23221 _ => {
23222 return Err(
23223 self.err("SEARCH BY column maps to a non-expression select item".into())
23224 );
23225 }
23226 };
23227 if srch.depth_first {
23228 // base: ARRAY[key]; rec: array_append(cte.set, key).
23229 body.items.push(SelectItem::Expr {
23230 expr: Expr::Array(alloc::vec![base_key]),
23231 alias: Some(srch.set_column.clone()),
23232 });
23233 body.unions[rec].1.items.push(SelectItem::Expr {
23234 expr: Expr::FunctionCall {
23235 name: "array_append".into(),
23236 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23237 },
23238 alias: Some(srch.set_column.clone()),
23239 });
23240 } else {
23241 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23242 // leading depth element dominates the element-wise comparison,
23243 // so shallower rows sort first, then by key — PG's (depth, key).
23244 body.items.push(SelectItem::Expr {
23245 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23246 alias: Some(srch.set_column.clone()),
23247 });
23248 // rec depth = cte.set[1] + 1.
23249 let parent_depth = Expr::ArraySubscript {
23250 target: Box::new(col_ref(&srch.set_column)),
23251 index: Box::new(Expr::Literal(Literal::Integer(1))),
23252 };
23253 body.unions[rec].1.items.push(SelectItem::Expr {
23254 expr: Expr::Array(alloc::vec![
23255 Expr::Binary {
23256 lhs: Box::new(parent_depth),
23257 op: BinOp::Add,
23258 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23259 },
23260 rec_key,
23261 ]),
23262 alias: Some(srch.set_column.clone()),
23263 });
23264 }
23265 extra_cols.push(srch.set_column);
23266 }
23267
23268 if let Some(cyc) = cycle {
23269 let positions: Vec<usize> = cyc
23270 .columns
23271 .iter()
23272 .map(|c| pos_of(c))
23273 .collect::<Result<_, _>>()?;
23274 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23275 // cast it to text for the cycle path: membership only needs equality,
23276 // and the record text form gives SPG a TextArray path (SPG has no
23277 // typed record[] array). Cycle detection is unaffected.
23278 let base_row = Expr::Cast {
23279 expr: Box::new(row_of(&body.items, &positions)?),
23280 target: CastTarget::Text,
23281 };
23282 let rec_row = Expr::Cast {
23283 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23284 target: CastTarget::Text,
23285 };
23286 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23287 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23288 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23289 body.items.push(SelectItem::Expr {
23290 expr: Expr::Literal(dflt.clone()),
23291 alias: Some(cyc.mark_column.clone()),
23292 });
23293 body.items.push(SelectItem::Expr {
23294 expr: Expr::Array(alloc::vec![base_row]),
23295 alias: Some(cyc.path_column.clone()),
23296 });
23297 // rec mark: ROW(cols) already in the path → cycle.
23298 let hit = Expr::AnyAll {
23299 expr: Box::new(rec_row.clone()),
23300 op: BinOp::Eq,
23301 array: Box::new(col_ref(&cyc.path_column)),
23302 is_any: true,
23303 };
23304 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23305 Expr::Case {
23306 operand: None,
23307 branches: alloc::vec![(hit, Expr::Literal(mark))],
23308 else_branch: Some(Box::new(Expr::Literal(dflt))),
23309 }
23310 } else {
23311 hit
23312 };
23313 body.unions[rec].1.items.push(SelectItem::Expr {
23314 expr: mark_expr,
23315 alias: Some(cyc.mark_column.clone()),
23316 });
23317 // rec path: array_append(cte.path, ROW(cols)).
23318 body.unions[rec].1.items.push(SelectItem::Expr {
23319 expr: Expr::FunctionCall {
23320 name: "array_append".into(),
23321 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23322 },
23323 alias: Some(cyc.path_column.clone()),
23324 });
23325 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23326 let stop = Expr::Unary {
23327 op: UnOp::Not,
23328 expr: Box::new(col_ref(&cyc.mark_column)),
23329 };
23330 let w = &mut body.unions[rec].1.where_;
23331 *w = Some(match w.take() {
23332 Some(prev) => Expr::Binary {
23333 lhs: Box::new(prev),
23334 op: BinOp::And,
23335 rhs: Box::new(stop),
23336 },
23337 None => stop,
23338 });
23339 extra_cols.push(cyc.mark_column);
23340 extra_cols.push(cyc.path_column);
23341 }
23342 cte.column_overrides.extend(extra_cols);
23343 Ok(())
23344 }
23345
23346 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23347 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23348 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23349 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23350 return Ok(None);
23351 }
23352 self.advance(); // SEARCH
23353 let depth_first = match self.peek() {
23354 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23355 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23356 other => {
23357 return Err(self.err(format!(
23358 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23359 )));
23360 }
23361 };
23362 self.advance();
23363 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23364 return Err(self.err(format!(
23365 "expected FIRST after SEARCH mode, got {:?}",
23366 self.peek()
23367 )));
23368 }
23369 self.advance();
23370 if !self.peek_is_by() {
23371 return Err(self.err(format!(
23372 "expected BY after SEARCH … FIRST, got {:?}",
23373 self.peek()
23374 )));
23375 }
23376 self.advance();
23377 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23378 while matches!(self.peek(), Token::Comma) {
23379 self.advance();
23380 by_columns.push(self.expect_ident_like()?);
23381 }
23382 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23383 return Err(self.err(format!(
23384 "expected SET in SEARCH clause, got {:?}",
23385 self.peek()
23386 )));
23387 }
23388 self.advance();
23389 let set_column = self.expect_ident_like()?;
23390 Ok(Some(crate::ast::SearchClause {
23391 depth_first,
23392 by_columns,
23393 set_column,
23394 }))
23395 }
23396
23397 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23398 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23399 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23400 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23401 return Ok(None);
23402 }
23403 self.advance(); // CYCLE
23404 let mut columns = alloc::vec![self.expect_ident_like()?];
23405 while matches!(self.peek(), Token::Comma) {
23406 self.advance();
23407 columns.push(self.expect_ident_like()?);
23408 }
23409 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23410 return Err(self.err(format!(
23411 "expected SET in CYCLE clause, got {:?}",
23412 self.peek()
23413 )));
23414 }
23415 self.advance();
23416 let mark_column = self.expect_ident_like()?;
23417 let mut mark_value = None;
23418 let mut default_value = None;
23419 if matches!(self.peek(), Token::To) {
23420 self.advance();
23421 mark_value = Some(self.parse_cycle_literal()?);
23422 if !matches!(self.peek(), Token::Default) {
23423 return Err(self.err(format!(
23424 "expected DEFAULT after CYCLE … TO, got {:?}",
23425 self.peek()
23426 )));
23427 }
23428 self.advance();
23429 default_value = Some(self.parse_cycle_literal()?);
23430 }
23431 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23432 return Err(self.err(format!(
23433 "expected USING in CYCLE clause, got {:?}",
23434 self.peek()
23435 )));
23436 }
23437 self.advance();
23438 let path_column = self.expect_ident_like()?;
23439 Ok(Some(crate::ast::CycleClause {
23440 columns,
23441 mark_column,
23442 mark_value,
23443 default_value,
23444 path_column,
23445 }))
23446 }
23447
23448 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23449 /// literal (string / bool / number) in PG.
23450 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23451 match self.parse_expr(0)? {
23452 Expr::Literal(l) => Ok(l),
23453 other => Err(self.err(format!(
23454 "CYCLE mark/default value must be a literal, got {other:?}"
23455 ))),
23456 }
23457 }
23458
23459 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23460 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23461 // Comes through as an identifier; consume it if present and
23462 // mark every CTE in the clause as recursive (PG semantics —
23463 // the flag is per-WITH, not per-CTE).
23464 let mut recursive = false;
23465 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23466 && s.eq_ignore_ascii_case("recursive")
23467 {
23468 self.advance();
23469 recursive = true;
23470 }
23471 let mut ctes = Vec::new();
23472 loop {
23473 let name = self.expect_ident_like()?;
23474 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23475 // PG uses these to rename the body's output columns; we
23476 // do the same below by overriding `columns[i].name`.
23477 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23478 self.advance();
23479 let mut names = Vec::new();
23480 loop {
23481 names.push(self.expect_ident_like()?);
23482 if matches!(self.peek(), Token::Comma) {
23483 self.advance();
23484 continue;
23485 }
23486 break;
23487 }
23488 if !matches!(self.peek(), Token::RParen) {
23489 return Err(self.err(format!(
23490 "expected ')' to close CTE column list, got {:?}",
23491 self.peek()
23492 )));
23493 }
23494 self.advance();
23495 names
23496 } else {
23497 Vec::new()
23498 };
23499 // AS is a reserved Token::As (used by SELECT-item / FROM
23500 // aliasing) — handle it specially rather than as a bare
23501 // ident.
23502 if !matches!(self.peek(), Token::As) {
23503 return Err(self.err(format!(
23504 "expected AS after CTE name {name:?}, got {:?}",
23505 self.peek()
23506 )));
23507 }
23508 self.advance();
23509 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23510 // MATERIALIZED` optimizer hints. SPG materialises every
23511 // CTE, so both spellings are accepted and absorbed.
23512 if matches!(self.peek(), Token::Not) {
23513 self.advance(); // NOT
23514 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23515 if s.eq_ignore_ascii_case("materialized"))
23516 {
23517 self.advance();
23518 } else {
23519 return Err(self.err(format!(
23520 "expected MATERIALIZED after AS NOT, got {:?}",
23521 self.peek()
23522 )));
23523 }
23524 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23525 if s.eq_ignore_ascii_case("materialized"))
23526 {
23527 self.advance();
23528 }
23529 if !matches!(self.peek(), Token::LParen) {
23530 return Err(self.err(format!(
23531 "expected '(' after AS in WITH clause, got {:?}",
23532 self.peek()
23533 )));
23534 }
23535 self.advance();
23536 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23537 // RETURNING) as the CTE body in addition to SELECT.
23538 // PG writable CTE semantics. UPDATE / DELETE come in as
23539 // bare Idents (lexer keeps SELECT / INSERT as reserved
23540 // tokens but treats the rest of DML as case-insensitive
23541 // idents).
23542 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23543 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23544 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23545 let body = match self.peek() {
23546 Token::Select => {
23547 let inner = self.parse_select_stmt()?;
23548 let Statement::Select(s) = inner else {
23549 unreachable!("parse_select_stmt returns Select");
23550 };
23551 crate::ast::CteBody::Select(s)
23552 }
23553 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23554 // `SELECT * FROM t` this way and accepts it wherever a
23555 // SELECT goes, so the CTE body dispatch needs its own
23556 // arm: this match is keyed on the FIRST token, and
23557 // `Token::Table` fell through to a tail that then
23558 // rejected what it got. `parse_table_shorthand` has
23559 // returned a desugared SelectStatement since the
23560 // shorthand landed — only the routing was missing.
23561 // Round 868 found this by putting the shorthand in a
23562 // subquery; every earlier check used a top-level form.
23563 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23564 // `SELECT * FROM t` this way and accepts it wherever a
23565 // SELECT goes, so the CTE body dispatch needs its own
23566 // arm: this match is keyed on the FIRST token, and
23567 // `Token::Table` fell through to a tail that rejected
23568 // what it got. `parse_table_shorthand` has returned a
23569 // desugared SelectStatement since the shorthand landed —
23570 // only the routing was missing, here and in the derived
23571 // table's second-token gate. Round 868 found both by
23572 // putting the shorthand in a subquery; every earlier
23573 // check had used a top-level form.
23574 Token::Table
23575 if matches!(
23576 self.tokens.get(self.pos + 1),
23577 Some(Token::Ident(_) | Token::QuotedIdent(_))
23578 ) =>
23579 {
23580 let mut head = self.parse_table_shorthand()?;
23581 self.parse_setop_chain_into(&mut head)?;
23582 self.parse_select_tail_into(&mut head)?;
23583 crate::ast::CteBody::Select(head)
23584 }
23585 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23586 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23587 // the shared rows helper onto a Select body.
23588 Token::Values => {
23589 self.advance(); // VALUES
23590 let mut head = self.parse_values_rows_body()?;
23591 // A VALUES seed can head a set-operation chain —
23592 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23593 // SELECT n+1 FROM t …). Attach any trailing
23594 // UNION / INTERSECT / EXCEPT peers so the
23595 // recursive-CTE body parses like the SELECT seed.
23596 self.parse_setop_chain_into(&mut head)?;
23597 crate::ast::CteBody::Select(head)
23598 }
23599 Token::Insert => {
23600 let inner = self.parse_one_statement()?;
23601 let Statement::Insert(s) = inner else {
23602 unreachable!("Token::Insert routes to Insert");
23603 };
23604 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23605 }
23606 _ if is_update_kw => {
23607 let inner = self.parse_one_statement()?;
23608 let Statement::Update(s) = inner else {
23609 return Err(
23610 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23611 );
23612 };
23613 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23614 }
23615 _ if is_delete_kw => {
23616 let inner = self.parse_one_statement()?;
23617 let Statement::Delete(s) = inner else {
23618 return Err(
23619 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23620 );
23621 };
23622 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23623 }
23624 // v7.39 (round 149) — PG 17 allows MERGE as a
23625 // data-modifying CTE body.
23626 _ if is_merge_kw => {
23627 let inner = self.parse_one_statement()?;
23628 let Statement::Merge(s) = inner else {
23629 return Err(
23630 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23631 );
23632 };
23633 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23634 }
23635 // v7.39 (round 151) — a CTE body may itself be
23636 // WITH-headed (PG grammar: PreparableStmt carries its
23637 // own with_clause). The nested statement keeps its own
23638 // ctes; the modifying-CTE-at-top-level rule is enforced
23639 // at execution.
23640 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23641 self.advance(); // WITH
23642 match self.parse_with_cte_then_select()? {
23643 Statement::Select(s) => crate::ast::CteBody::Select(s),
23644 Statement::Insert(s) => {
23645 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23646 }
23647 Statement::Update(s) => {
23648 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23649 }
23650 Statement::Delete(s) => {
23651 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23652 }
23653 Statement::Merge(s) => {
23654 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23655 }
23656
23657 other => {
23658 return Err(self.err(format!(
23659 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23660 )));
23661 }
23662 }
23663 }
23664 other => {
23665 return Err(self.err(format!(
23666 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23667 )));
23668 }
23669 };
23670 if !matches!(self.peek(), Token::RParen) {
23671 return Err(self.err(format!(
23672 "expected ')' after CTE body, got {:?}",
23673 self.peek()
23674 )));
23675 }
23676 self.advance();
23677 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23678 // CTE, desugared into extra body columns by the engine.
23679 let search = self.parse_cte_search_clause()?;
23680 let cycle = self.parse_cte_cycle_clause()?;
23681 let mut cte = crate::ast::Cte {
23682 name,
23683 body,
23684 recursive,
23685 column_overrides,
23686 search,
23687 cycle,
23688 };
23689 self.validate_recursive_cte(&cte)?;
23690 self.desugar_cte_search_cycle(&mut cte)?;
23691 ctes.push(cte);
23692 if matches!(self.peek(), Token::Comma) {
23693 self.advance();
23694 continue;
23695 }
23696 break;
23697 }
23698 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23699 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23700 // the parsed CTEs to whichever statement the body produces.
23701 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23702 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23703 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23704 match self.peek() {
23705 Token::Select => {
23706 let body_stmt = self.parse_select_stmt()?;
23707 let Statement::Select(mut body) = body_stmt else {
23708 unreachable!()
23709 };
23710 body.ctes = ctes;
23711 Ok(Statement::Select(body))
23712 }
23713 Token::Insert => {
23714 let body_stmt = self.parse_one_statement()?;
23715 let Statement::Insert(mut body) = body_stmt else {
23716 unreachable!()
23717 };
23718 body.ctes = ctes;
23719 Ok(Statement::Insert(body))
23720 }
23721 _ if outer_is_update => {
23722 let body_stmt = self.parse_one_statement()?;
23723 let Statement::Update(mut body) = body_stmt else {
23724 return Err(self.err(format!("expected UPDATE after WITH clause")));
23725 };
23726 body.ctes = ctes;
23727 Ok(Statement::Update(body))
23728 }
23729 _ if outer_is_delete => {
23730 let body_stmt = self.parse_one_statement()?;
23731 let Statement::Delete(mut body) = body_stmt else {
23732 return Err(self.err(format!("expected DELETE after WITH clause")));
23733 };
23734 body.ctes = ctes;
23735 Ok(Statement::Delete(body))
23736 }
23737 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23738 // WITH RECURSIVE is rejected with PG's exact message
23739 // (parse analysis, transformWithClause).
23740 _ if outer_is_merge => {
23741 if recursive {
23742 return Err(self.err(String::from(
23743 "WITH RECURSIVE is not supported for MERGE statement",
23744 )));
23745 }
23746 let body_stmt = self.parse_one_statement()?;
23747 let Statement::Merge(mut body) = body_stmt else {
23748 return Err(self.err(format!("expected MERGE after WITH clause")));
23749 };
23750 body.ctes = ctes;
23751 Ok(Statement::Merge(body))
23752 }
23753 other => Err(self.err(format!(
23754 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23755 ))),
23756 }
23757 }
23758
23759 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23760 /// already consumed the leading `EXISTS` ident via
23761 /// `self.advance()`.
23762 /// v7.13.0 — parse the rest of a `CASE … END` expression after
23763 /// the leading `CASE` ident has been consumed (mailrs round-5
23764 /// G9). Supports both the searched form
23765 /// (`CASE WHEN cond THEN val …`) and the simple form
23766 /// (`CASE operand WHEN val THEN val …`).
23767 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23768 // Disambiguate searched vs simple form: if the next token
23769 // is `WHEN`, we're in the searched form. Otherwise the
23770 // intervening expression is the operand.
23771 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23772 None
23773 } else {
23774 Some(Box::new(self.parse_expr(0)?))
23775 };
23776 let mut branches: Vec<(Expr, Expr)> = Vec::new();
23777 loop {
23778 match self.peek() {
23779 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23780 self.advance();
23781 let cond = self.parse_expr(0)?;
23782 match self.peek() {
23783 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23784 self.advance();
23785 }
23786 other => {
23787 return Err(self.err(alloc::format!(
23788 "expected THEN after CASE WHEN <expr>, got {other:?}"
23789 )));
23790 }
23791 }
23792 let value = self.parse_expr(0)?;
23793 branches.push((cond, value));
23794 }
23795 _ => break,
23796 }
23797 }
23798 if branches.is_empty() {
23799 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23800 }
23801 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23802 {
23803 self.advance();
23804 Some(Box::new(self.parse_expr(0)?))
23805 } else {
23806 None
23807 };
23808 match self.peek() {
23809 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23810 self.advance();
23811 }
23812 other => {
23813 return Err(self.err(alloc::format!(
23814 "expected END to close CASE expression, got {other:?}"
23815 )));
23816 }
23817 }
23818 Ok(Expr::Case {
23819 operand,
23820 branches,
23821 else_branch,
23822 })
23823 }
23824
23825 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23826 /// query-source position (EXISTS / IN / INSERT source / CTE body /
23827 /// view body). Caller consumed the WITH keyword. Only a SELECT
23828 /// outer is grammatical here; the data-modifying-CTE-at-top-level
23829 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23830 /// maps correctly.
23831 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23832 let inner = self.parse_with_cte_then_select()?;
23833 match inner {
23834 Statement::Select(s) => Ok(s),
23835 other => Err(self.err(format!(
23836 "expected SELECT after WITH in a subquery, got {other:?}"
23837 ))),
23838 }
23839 }
23840
23841 /// True when the next token is the (unquoted) WITH keyword. WITH is
23842 /// reserved in PG, so a bare `with` can never be a column reference
23843 /// in these positions; a quoted `"with"` stays an identifier.
23844 fn peek_is_with_kw(&self) -> bool {
23845 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23846 }
23847
23848 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23849 /// `#[inline(never)]` keeps the large SelectStatement temporaries
23850 /// off parse_expr's recursive frame (the nesting-budget stack
23851 /// cliff — see the round-153 gate regression).
23852 #[inline(never)]
23853 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23854 if self.peek_is_with_kw() {
23855 self.advance();
23856 self.parse_nested_with_select()
23857 } else {
23858 match self.parse_select_stmt()? {
23859 Statement::Select(s) => Ok(s),
23860 other => Err(self.err(alloc::format!(
23861 "expected SELECT inside ANY/ALL, got {other:?}"
23862 ))),
23863 }
23864 }
23865 }
23866
23867 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23868 if !matches!(self.peek(), Token::LParen) {
23869 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23870 }
23871 self.advance();
23872 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23873 let s = if self.peek_is_with_kw() {
23874 self.advance();
23875 self.parse_nested_with_select()?
23876 } else {
23877 let inner = self.parse_select_stmt()?;
23878 let Statement::Select(s) = inner else {
23879 unreachable!("parse_select_stmt returns Select")
23880 };
23881 s
23882 };
23883 if !matches!(self.peek(), Token::RParen) {
23884 return Err(self.err(format!(
23885 "expected ')' after EXISTS-subquery, got {:?}",
23886 self.peek()
23887 )));
23888 }
23889 self.advance();
23890 Ok(Expr::Exists {
23891 subquery: Box::new(s),
23892 negated,
23893 })
23894 }
23895
23896 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23897 self.advance(); // IN
23898 if !matches!(self.peek(), Token::LParen) {
23899 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23900 }
23901 self.advance();
23902 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23903 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23904 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23905 let s = if self.peek_is_with_kw() {
23906 self.advance();
23907 self.parse_nested_with_select()?
23908 } else {
23909 let inner = self.parse_select_stmt()?;
23910 let Statement::Select(s) = inner else {
23911 unreachable!("parse_select_stmt always returns Statement::Select")
23912 };
23913 s
23914 };
23915 if !matches!(self.peek(), Token::RParen) {
23916 return Err(self.err(format!(
23917 "expected ')' after IN-subquery, got {:?}",
23918 self.peek()
23919 )));
23920 }
23921 self.advance();
23922 return Ok(Expr::InSubquery {
23923 expr: Box::new(expr),
23924 subquery: Box::new(s),
23925 negated,
23926 });
23927 }
23928 let mut elements = Vec::new();
23929 if !matches!(self.peek(), Token::RParen) {
23930 loop {
23931 elements.push(self.parse_expr(0)?);
23932 match self.peek() {
23933 Token::Comma => {
23934 self.advance();
23935 }
23936 Token::RParen => break,
23937 other => {
23938 return Err(
23939 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23940 );
23941 }
23942 }
23943 }
23944 }
23945 self.advance(); // ')'
23946 // v7.30.2 (mailrs round-25) — flat InList node instead of a
23947 // left-deep OR-Eq chain: chain depth scaled with the element
23948 // count and overflowed the stack (eval + drop are recursive).
23949 if elements.is_empty() {
23950 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23951 }
23952 Ok(Expr::InList {
23953 expr: Box::new(expr),
23954 list: elements,
23955 negated,
23956 })
23957 }
23958
23959 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23960 /// already consumed by the caller. Elements must be numeric literals
23961 /// (with optional unary `-`); any compound expression is rejected at
23962 /// parse time so the runtime never needs to evaluate inside a vector.
23963 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23964 /// has already consumed the `EXTRACT` token before calling us —
23965 /// we pick up at the opening `(`.
23966 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23967 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23968 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23969 /// per-column OR-fold of
23970 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23971 /// term)` so the existing FTS evaluator handles semantics.
23972 ///
23973 /// The mode modifier is accepted-and-ignored at v7.17 — all
23974 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23975 /// mode operators (`+foo -bar`) would need their own parser
23976 /// (Phase 2.2c); customers who hit them today already get a
23977 /// correct lexeme-match against the bare term, only without
23978 /// the +/- precedence the customer asked for.
23979 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23980 // Already at `MATCH`-consumed position; the dispatcher
23981 // confirmed the next token is `(`.
23982 if !matches!(self.peek(), Token::LParen) {
23983 return Err(self.err(alloc::format!(
23984 "expected '(' after MATCH, got {:?}",
23985 self.peek()
23986 )));
23987 }
23988 self.advance();
23989 let mut cols: Vec<Expr> = Vec::new();
23990 loop {
23991 cols.push(self.parse_expr(0)?);
23992 match self.peek() {
23993 Token::Comma => {
23994 self.advance();
23995 }
23996 Token::RParen => break,
23997 other => {
23998 return Err(self.err(alloc::format!(
23999 "expected ',' or ')' in MATCH column list, got {other:?}"
24000 )));
24001 }
24002 }
24003 }
24004 self.advance(); // ')'
24005 // Expect AGAINST.
24006 match self.peek() {
24007 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24008 self.advance();
24009 }
24010 other => {
24011 return Err(self.err(alloc::format!(
24012 "expected AGAINST after MATCH column list, got {other:?}"
24013 )));
24014 }
24015 }
24016 if !matches!(self.peek(), Token::LParen) {
24017 return Err(self.err(alloc::format!(
24018 "expected '(' after AGAINST, got {:?}",
24019 self.peek()
24020 )));
24021 }
24022 self.advance();
24023 // Read AGAINST's argument as a single primary token —
24024 // string literal, placeholder, or column-ref ident. We
24025 // can't call `parse_expr` / `parse_unary` here because
24026 // the postfix chain inside `parse_atom` would greedily
24027 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24028 // and fail at "expected '(' after IN". Customers always
24029 // write a literal or bound parameter in AGAINST, so this
24030 // restriction is non-blocking; the error path explains
24031 // the limit if a more complex expression shows up.
24032 let term = match self.advance() {
24033 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24034 Token::Placeholder(n) => Expr::Placeholder(n),
24035 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24036 qualifier: None,
24037 name: s,
24038 }),
24039 other => {
24040 return Err(self.err(alloc::format!(
24041 "MATCH ... AGAINST(<term>) expects a string literal, \
24042 bound parameter, or column ref, got {other:?}"
24043 )));
24044 }
24045 };
24046 // Optional mode tail — accept-and-ignore at v7.17:
24047 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24048 // IN BOOLEAN MODE
24049 // WITH QUERY EXPANSION
24050 loop {
24051 match self.peek() {
24052 // IN lexes as a reserved Token::In, not an ident,
24053 // so it gets its own arm.
24054 Token::In => {
24055 self.advance();
24056 }
24057 Token::Ident(s) | Token::QuotedIdent(s)
24058 if s.eq_ignore_ascii_case("natural")
24059 || s.eq_ignore_ascii_case("language")
24060 || s.eq_ignore_ascii_case("boolean")
24061 || s.eq_ignore_ascii_case("mode")
24062 || s.eq_ignore_ascii_case("with")
24063 || s.eq_ignore_ascii_case("query")
24064 || s.eq_ignore_ascii_case("expansion") =>
24065 {
24066 self.advance();
24067 }
24068 _ => break,
24069 }
24070 }
24071 if !matches!(self.peek(), Token::RParen) {
24072 return Err(self.err(alloc::format!(
24073 "expected ')' to close AGAINST, got {:?}",
24074 self.peek()
24075 )));
24076 }
24077 self.advance();
24078 // Build per-column `to_tsvector('simple', col) @@
24079 // plainto_tsquery('simple', term)` and OR-fold.
24080 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24081 let plainto = Expr::FunctionCall {
24082 name: String::from("plainto_tsquery"),
24083 args: alloc::vec![simple_lit(), term.clone()],
24084 };
24085 let mut folded: Option<Expr> = None;
24086 for col in cols {
24087 let to_tsv = Expr::FunctionCall {
24088 name: String::from("to_tsvector"),
24089 args: alloc::vec![simple_lit(), col],
24090 };
24091 let leaf = Expr::Binary {
24092 lhs: Box::new(to_tsv),
24093 op: crate::ast::BinOp::TsMatch,
24094 rhs: Box::new(plainto.clone()),
24095 };
24096 folded = Some(match folded {
24097 None => leaf,
24098 Some(prev) => Expr::Binary {
24099 lhs: Box::new(prev),
24100 op: crate::ast::BinOp::Or,
24101 rhs: Box::new(leaf),
24102 },
24103 });
24104 }
24105 match folded {
24106 Some(e) => Ok(e),
24107 None => Err(self.err(String::from(
24108 "MATCH(...) AGAINST(...) requires at least one column",
24109 ))),
24110 }
24111 }
24112
24113 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24114 if !matches!(self.peek(), Token::LParen) {
24115 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24116 }
24117 self.advance();
24118 let field_name = self.expect_ident_like()?;
24119 let field = match field_name.to_ascii_lowercase().as_str() {
24120 // PG accepts the plural spellings (years/months/…/millenniums) as
24121 // aliases for the singular fields — its datetime unit table has both.
24122 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24123 "year" | "years" => ExtractField::Year,
24124 "month" | "months" => ExtractField::Month,
24125 "day" | "days" => ExtractField::Day,
24126 "hour" | "hours" => ExtractField::Hour,
24127 "minute" | "minutes" => ExtractField::Minute,
24128 "second" | "seconds" => ExtractField::Second,
24129 "microsecond" | "microseconds" => ExtractField::Microsecond,
24130 "epoch" => ExtractField::Epoch,
24131 "dow" => ExtractField::Dow,
24132 "isodow" => ExtractField::Isodow,
24133 "doy" => ExtractField::Doy,
24134 "week" | "weeks" => ExtractField::Week,
24135 "isoyear" => ExtractField::Isoyear,
24136 "quarter" => ExtractField::Quarter,
24137 "decade" | "decades" => ExtractField::Decade,
24138 "century" | "centuries" => ExtractField::Century,
24139 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24140 "julian" => ExtractField::Julian,
24141 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24142 "timezone" => ExtractField::Timezone,
24143 "timezone_hour" => ExtractField::TimezoneHour,
24144 "timezone_minute" => ExtractField::TimezoneMinute,
24145 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24146 // reports an unknown one with the source type (22023); carry the
24147 // raw name so eval can word it.
24148 other => ExtractField::Other(alloc::string::String::from(other)),
24149 };
24150 if !matches!(self.peek(), Token::From) {
24151 return Err(self.err(format!(
24152 "expected FROM after EXTRACT field, got {:?}",
24153 self.peek()
24154 )));
24155 }
24156 self.advance();
24157 let source = self.parse_expr(0)?;
24158 if !matches!(self.peek(), Token::RParen) {
24159 return Err(self.err(format!(
24160 "expected ')' to close EXTRACT, got {:?}",
24161 self.peek()
24162 )));
24163 }
24164 self.advance();
24165 Ok(Expr::Extract {
24166 field,
24167 source: Box::new(source),
24168 })
24169 }
24170
24171 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24172 /// is already consumed; we expect a single string literal next and
24173 /// resolve it into `Literal::Interval` at parse time so the engine
24174 /// never has to re-tokenise inside the string.
24175 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24176 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24177 /// is the SQL-standard form and is left to the path below.
24178 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24179 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24180 let (offset, sign) = match self.peek() {
24181 Token::Minus => (1, "-"),
24182 _ => (0, ""),
24183 };
24184 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24185 return None;
24186 };
24187 self.tokens
24188 .get(self.pos + offset + 1)
24189 .filter(|t| mysql_interval_unit(t).is_some())?;
24190 Some((alloc::format!("{sign}{n}"), offset + 1))
24191 }
24192
24193 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24194 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24195 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24196 ///
24197 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24198 /// this by parsing the group and then restoring `self.pos` — which could
24199 /// never have worked, because `advance()` DESTROYS the token it returns
24200 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24201 /// inert only because both branches errored back then.
24202 fn interval_paren_is_quantity(&self) -> bool {
24203 let mut depth = 0usize;
24204 let mut saw_top_level_comma = false;
24205 let mut i = self.pos;
24206 while let Some(tok) = self.tokens.get(i) {
24207 match tok {
24208 Token::LParen => depth += 1,
24209 Token::RParen => {
24210 depth = depth.saturating_sub(1);
24211 if depth == 0 {
24212 return !saw_top_level_comma
24213 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24214 .is_some();
24215 }
24216 }
24217 // A comma directly inside the outermost parens means the
24218 // argument list of the INTERVAL() function.
24219 Token::Comma if depth == 1 => saw_top_level_comma = true,
24220 Token::Eof => return false,
24221 _ => {}
24222 }
24223 i += 1;
24224 }
24225 false
24226 }
24227
24228 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24229 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24230 // (the index of the last Ni ≤ N), distinct from the interval literal.
24231 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24232 // is decided by a non-destructive lookahead (round 422) before either
24233 // branch consumes anything. MySQL only.
24234 if self.mysql_dialect
24235 && matches!(self.peek(), Token::LParen)
24236 && !self.interval_paren_is_quantity()
24237 {
24238 self.advance(); // (
24239 let mut args = Vec::new();
24240 if !matches!(self.peek(), Token::RParen) {
24241 loop {
24242 args.push(self.parse_expr(0)?);
24243 if matches!(self.peek(), Token::Comma) {
24244 self.advance();
24245 continue;
24246 }
24247 break;
24248 }
24249 }
24250 if !matches!(self.peek(), Token::RParen) {
24251 return Err(self.err(alloc::format!(
24252 "expected ')' after INTERVAL() arguments, got {:?}",
24253 self.peek()
24254 )));
24255 }
24256 self.advance(); // )
24257 return Ok(Expr::FunctionCall {
24258 name: alloc::string::String::from("interval"),
24259 args,
24260 });
24261 }
24262 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24263 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24264 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24265 // writes every date arithmetic there is, and it did not parse at
24266 // all. PG rejects the unquoted form outright (`syntax error at or
24267 // near "1"`, measured), so it is taken only in the MySQL dialect —
24268 // PG's own `INTERVAL '1' DAY` is untouched below.
24269 if self.mysql_dialect
24270 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24271 {
24272 for _ in 0..consume {
24273 self.advance(); // the optional `-` and the number
24274 }
24275 let Some(unit) = mysql_interval_unit(self.peek()) else {
24276 return Err(self.err(alloc::format!(
24277 "expected an interval unit after INTERVAL {text}, got {:?}",
24278 self.peek()
24279 )));
24280 };
24281 self.advance(); // the unit
24282 let (months, days, micros) = scale_mysql_interval(&text, unit)
24283 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24284 return Ok(Expr::Literal(Literal::Interval {
24285 months,
24286 days,
24287 micros,
24288 // The canonical rendering, so Display round-trips into a
24289 // form both dialects read back.
24290 text: alloc::format!("{text} {unit}"),
24291 }));
24292 }
24293 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24294 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24295 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24296 // Those cannot fold into a compile-time `Literal::Interval`, so they
24297 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24298 // builtin, which builds the value at run time (and yields NULL for a
24299 // NULL quantity, as MariaDB does). The literal path above still folds
24300 // the constant case — it is cheaper and round-trips through Display.
24301 //
24302 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24303 // MySQL's quoted spelling) keep the qualifier path below.
24304 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24305 let qty = self.parse_expr(0)?;
24306 let Some(unit) = mysql_interval_unit(self.peek()) else {
24307 return Err(self.err(alloc::format!(
24308 "expected an interval unit after INTERVAL <expr>, got {:?}",
24309 self.peek()
24310 )));
24311 };
24312 self.advance(); // the unit
24313 return Ok(make_interval_call(qty, unit));
24314 }
24315 let tok = self.advance();
24316 let Token::String(text) = tok else {
24317 return Err(self.err(format!(
24318 "expected string literal after INTERVAL, got {tok:?}"
24319 )));
24320 };
24321 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24322 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24323 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24324 // bare number means and the leading/trailing precision.
24325 let field1 = interval_field_of(self.peek());
24326 let qualifier = if let Some(f1) = field1 {
24327 self.advance();
24328 let f2 = if matches!(self.peek(), Token::To) {
24329 self.advance();
24330 let Some(f) = interval_field_of(self.peek()) else {
24331 return Err(self.err(format!(
24332 "expected an interval field after TO, got {:?}",
24333 self.peek()
24334 )));
24335 };
24336 self.advance();
24337 Some(f)
24338 } else {
24339 None
24340 };
24341 Some((f1, f2))
24342 } else {
24343 None
24344 };
24345 let (months, days, micros) = match qualifier {
24346 Some(q) => interpret_qualified_interval(&text, q),
24347 None => parse_interval_text(&text),
24348 }
24349 .ok_or_else(|| ParseError {
24350 message: format!(
24351 "cannot parse INTERVAL {text:?}; \
24352 expected `<n> <unit> [<n> <unit> ...]` with units \
24353 microsecond[s], millisecond[s], second[s], minute[s], \
24354 hour[s], day[s], week[s], month[s], year[s]"
24355 ),
24356 token_pos: self.consumed_pos(),
24357 })?;
24358 Ok(Expr::Literal(Literal::Interval {
24359 months,
24360 days,
24361 micros,
24362 text,
24363 }))
24364 }
24365
24366 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24367 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24368 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24369 /// than a pgvector literal.
24370 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24371 self.advance(); // consume `[`
24372 let mut items: Vec<Expr> = Vec::new();
24373 if !matches!(self.peek(), Token::RBracket) {
24374 loop {
24375 if matches!(self.peek(), Token::LBracket) {
24376 items.push(self.parse_array_bracket_body()?);
24377 } else {
24378 items.push(self.parse_expr(0)?);
24379 }
24380 match self.peek() {
24381 Token::Comma => {
24382 self.advance();
24383 }
24384 Token::RBracket => break,
24385 other => {
24386 return Err(self.err(alloc::format!(
24387 "expected ',' or ']' in array literal, got {other:?}"
24388 )));
24389 }
24390 }
24391 }
24392 }
24393 self.advance(); // consume `]`
24394 Ok(Expr::Array(items))
24395 }
24396
24397 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24398 let mut elems = Vec::new();
24399 if matches!(self.peek(), Token::RBracket) {
24400 self.advance();
24401 return Ok(Expr::Literal(Literal::Vector(elems)));
24402 }
24403 loop {
24404 let e = self.parse_expr(0)?;
24405 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24406 message: format!("vector element must be a numeric literal, got {e:?}"),
24407 token_pos: self.pos,
24408 })?;
24409 elems.push(x);
24410 match self.peek() {
24411 Token::Comma => {
24412 self.advance();
24413 }
24414 Token::RBracket => {
24415 self.advance();
24416 break;
24417 }
24418 other => {
24419 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24420 }
24421 }
24422 }
24423 Ok(Expr::Literal(Literal::Vector(elems)))
24424 }
24425
24426 /// Atom that started with an identifier: could be `t.col`, `col`, or
24427 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24428 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24429 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24430 /// is optional; an empty `()` is also legal (PG semantics).
24431 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24432 /// modifier between `name(args)` and `OVER (...)`. Default is
24433 /// `Respect`. Unrecognised idents leave the stream unchanged.
24434 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24435 let Token::Ident(s) = self.peek().clone() else {
24436 return NullTreatment::Respect;
24437 };
24438 let is_ignore = s.eq_ignore_ascii_case("ignore");
24439 let is_respect = s.eq_ignore_ascii_case("respect");
24440 if !is_ignore && !is_respect {
24441 return NullTreatment::Respect;
24442 }
24443 // Lookahead for NULLS — only consume both tokens together.
24444 // pos+1 must hold a "nulls" ident.
24445 if self.pos + 1 < self.tokens.len()
24446 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24447 && s2.eq_ignore_ascii_case("nulls")
24448 {
24449 self.advance();
24450 self.advance();
24451 return if is_ignore {
24452 NullTreatment::Ignore
24453 } else {
24454 NullTreatment::Respect
24455 };
24456 }
24457 NullTreatment::Respect
24458 }
24459
24460 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24461 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24462 /// (same shape as the `OVER` tail). Consumes the whole clause and
24463 /// returns the predicate; returns `None` when no `FILTER` follows.
24464 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24465 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24466 return Ok(None);
24467 };
24468 if !s.eq_ignore_ascii_case("filter") {
24469 return Ok(None);
24470 }
24471 self.advance(); // FILTER
24472 if !matches!(self.peek(), Token::LParen) {
24473 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24474 }
24475 self.advance(); // (
24476 if !matches!(self.peek(), Token::Where) {
24477 return Err(self.err(format!(
24478 "expected WHERE inside FILTER (...), got {:?}",
24479 self.peek()
24480 )));
24481 }
24482 self.advance(); // WHERE
24483 let cond = self.parse_expr(0)?;
24484 if !matches!(self.peek(), Token::RParen) {
24485 return Err(self.err(format!(
24486 "expected ')' to close FILTER (WHERE ...), got {:?}",
24487 self.peek()
24488 )));
24489 }
24490 self.advance(); // )
24491 Ok(Some(Box::new(cond)))
24492 }
24493
24494 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24495 /// the separator as the aggregate's second argument, which is the
24496 /// shape `string_agg` already takes. Returns whether one was there.
24497 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24498 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24499 return Ok(false);
24500 }
24501 self.advance();
24502 let Token::String(sep) = self.peek().clone() else {
24503 return Err(self.err(alloc::format!(
24504 "expected a string literal after SEPARATOR, got {:?}",
24505 self.peek()
24506 )));
24507 };
24508 self.advance();
24509 args.push(Expr::Literal(Literal::String(sep)));
24510 Ok(true)
24511 }
24512
24513 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24514 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24515 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24516 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24517 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24518 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24519 return Ok(Vec::new());
24520 };
24521 if !s.eq_ignore_ascii_case("within") {
24522 return Ok(Vec::new());
24523 }
24524 self.advance(); // WITHIN
24525 if !matches!(self.peek(), Token::Group) {
24526 return Err(self.err(format!(
24527 "expected GROUP after WITHIN, got {:?}",
24528 self.peek()
24529 )));
24530 }
24531 self.advance(); // GROUP
24532 if !matches!(self.peek(), Token::LParen) {
24533 return Err(self.err(format!(
24534 "expected '(' after WITHIN GROUP, got {:?}",
24535 self.peek()
24536 )));
24537 }
24538 self.advance(); // (
24539 if !matches!(self.peek(), Token::Order) {
24540 return Err(self.err(format!(
24541 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24542 self.peek()
24543 )));
24544 }
24545 self.advance(); // ORDER
24546 if !self.peek_is_by() {
24547 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24548 }
24549 self.advance(); // BY
24550 let mut keys: Vec<OrderBy> = Vec::new();
24551 loop {
24552 // v7.39 (round 691) — save/restore, the discipline this parser
24553 // already uses around `pending_sample_preds`, so a subquery inside
24554 // a key neither inherits nor leaks the channel.
24555 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24556 let saved_coll = self.order_key_collation.take();
24557 let parsed = self.parse_expr(0);
24558 self.in_order_by_key = saved_flag;
24559 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24560 let expr = parsed?;
24561 let desc = if matches!(self.peek(), Token::Desc) {
24562 self.advance();
24563 true
24564 } else if matches!(self.peek(), Token::Asc) {
24565 self.advance();
24566 false
24567 } else {
24568 false
24569 };
24570 let nulls_first = self.parse_optional_nulls_placement()?;
24571 keys.push(OrderBy {
24572 expr,
24573 desc,
24574 nulls_first,
24575 collation,
24576 });
24577 if matches!(self.peek(), Token::Comma) {
24578 self.advance();
24579 } else {
24580 break;
24581 }
24582 }
24583 if !matches!(self.peek(), Token::RParen) {
24584 return Err(self.err(format!(
24585 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24586 self.peek()
24587 )));
24588 }
24589 self.advance(); // )
24590 Ok(keys)
24591 }
24592
24593 /// No frame clause is supported.
24594 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24595 fn parse_over_clause(
24596 &mut self,
24597 ) -> Result<
24598 (
24599 Vec<Expr>,
24600 Vec<(Expr, bool, Option<bool>)>,
24601 Option<WindowFrame>,
24602 ),
24603 ParseError,
24604 > {
24605 // `OVER w` — a named-window reference. The WINDOW clause
24606 // parses after the select list, so the name rides out as a
24607 // marker in partition_by; parse_bare_select substitutes the
24608 // definition once the clause is known.
24609 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24610 let name = w.clone();
24611 self.advance();
24612 return Ok((
24613 alloc::vec![Expr::Column(crate::ast::ColumnName {
24614 qualifier: Some("__named_window__".to_string()),
24615 name,
24616 })],
24617 Vec::new(),
24618 None,
24619 ));
24620 }
24621 if !matches!(self.peek(), Token::LParen) {
24622 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24623 }
24624 self.advance();
24625 let mut partition_by = Vec::new();
24626 let mut order_by = Vec::new();
24627 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24628 // window, refined in place. PG's rules (probed against 18.4) differ
24629 // from the bare `OVER w1` form, so the reference rides out under its
24630 // own marker and `substitute_named_windows` applies them. The base
24631 // name is any leading identifier that isn't a window-spec keyword.
24632 let base_window = match self.peek() {
24633 Token::Ident(s) | Token::QuotedIdent(s)
24634 if !s.eq_ignore_ascii_case("partition")
24635 && !s.eq_ignore_ascii_case("rows")
24636 && !s.eq_ignore_ascii_case("range")
24637 && !s.eq_ignore_ascii_case("groups") =>
24638 {
24639 let n = s.clone();
24640 self.advance();
24641 Some(n)
24642 }
24643 _ => None,
24644 };
24645 // PARTITION BY ?
24646 // v7.37.6-B promoted PARTITION to a reserved keyword
24647 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24648 // `Token::Ident("partition")`. Accept both so older sources
24649 // and the new lexer surface land on the same path.
24650 let is_partition_kw = match self.peek() {
24651 Token::Partition => true,
24652 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24653 _ => false,
24654 };
24655 if is_partition_kw {
24656 self.advance();
24657 if !self.peek_is_by() {
24658 return Err(self.err(format!(
24659 "expected BY after PARTITION, got {:?}",
24660 self.peek()
24661 )));
24662 }
24663 self.advance();
24664 loop {
24665 partition_by.push(self.parse_expr(0)?);
24666 if matches!(self.peek(), Token::Comma) {
24667 self.advance();
24668 continue;
24669 }
24670 break;
24671 }
24672 }
24673 // ORDER BY ?
24674 if matches!(self.peek(), Token::Order) {
24675 self.advance();
24676 if !self.peek_is_by() {
24677 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24678 }
24679 self.advance();
24680 loop {
24681 let e = self.parse_expr(0)?;
24682 let desc = if matches!(self.peek(), Token::Desc) {
24683 self.advance();
24684 true
24685 } else if matches!(self.peek(), Token::Asc) {
24686 self.advance();
24687 false
24688 } else {
24689 false
24690 };
24691 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24692 let nulls_first = self.parse_optional_nulls_placement()?;
24693 order_by.push((e, desc, nulls_first));
24694 if matches!(self.peek(), Token::Comma) {
24695 self.advance();
24696 continue;
24697 }
24698 break;
24699 }
24700 }
24701 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24702 // Both keywords come through the lexer as identifiers; match
24703 // case-insensitively.
24704 let mut frame: Option<WindowFrame> = None;
24705 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24706 let kind = if s.eq_ignore_ascii_case("rows") {
24707 Some(FrameKind::Rows)
24708 } else if s.eq_ignore_ascii_case("range") {
24709 Some(FrameKind::Range)
24710 } else if s.eq_ignore_ascii_case("groups") {
24711 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24712 Some(FrameKind::Groups)
24713 } else {
24714 None
24715 };
24716 if let Some(kind) = kind {
24717 self.advance();
24718 frame = Some(self.parse_frame_tail(kind)?);
24719 }
24720 }
24721 if !matches!(self.peek(), Token::RParen) {
24722 return Err(self.err(format!(
24723 "expected ')' to close OVER clause, got {:?}",
24724 self.peek()
24725 )));
24726 }
24727 self.advance();
24728 if let Some(base) = base_window {
24729 // A copy may refine but never override the base's partitioning
24730 // (PG rejects it outright, before looking the name up).
24731 if !partition_by.is_empty() {
24732 return Err(self.err(alloc::format!(
24733 "cannot override PARTITION BY clause of window \"{base}\""
24734 )));
24735 }
24736 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24737 qualifier: Some("__named_window_ref__".to_string()),
24738 name: base,
24739 })];
24740 }
24741 Ok((partition_by, order_by, frame))
24742 }
24743
24744 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24745 /// or `RANGE` keyword was just consumed. Accepts both
24746 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24747 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24748 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24749 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24750 let (start, end) = if matches!(self.peek(), Token::Between) {
24751 self.advance();
24752 let start = self.parse_frame_bound()?;
24753 if !matches!(self.peek(), Token::And) {
24754 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24755 }
24756 self.advance();
24757 let end = self.parse_frame_bound()?;
24758 (start, Some(end))
24759 } else {
24760 (self.parse_frame_bound()?, None)
24761 };
24762 let exclude = self.parse_frame_exclusion()?;
24763 Ok(WindowFrame {
24764 kind,
24765 start,
24766 end,
24767 exclude,
24768 })
24769 }
24770
24771 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24772 /// after a frame spec. NO OTHERS is the default no-op.
24773 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24774 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24775 return Ok(FrameExclusion::NoOthers);
24776 }
24777 self.advance(); // EXCLUDE
24778 match self.peek() {
24779 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24780 self.advance();
24781 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24782 return Err(self.err(format!(
24783 "expected ROW after EXCLUDE CURRENT, got {:?}",
24784 self.peek()
24785 )));
24786 }
24787 self.advance();
24788 Ok(FrameExclusion::CurrentRow)
24789 }
24790 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24791 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24792 // Without this arm it fell to the catch-all, whose message
24793 // self-contradictingly listed GROUP as expected.
24794 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24795 self.advance();
24796 Ok(FrameExclusion::Group)
24797 }
24798 Token::Group => {
24799 self.advance();
24800 Ok(FrameExclusion::Group)
24801 }
24802 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24803 self.advance();
24804 Ok(FrameExclusion::Ties)
24805 }
24806 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24807 self.advance();
24808 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24809 return Err(self.err(format!(
24810 "expected OTHERS after EXCLUDE NO, got {:?}",
24811 self.peek()
24812 )));
24813 }
24814 self.advance();
24815 Ok(FrameExclusion::NoOthers)
24816 }
24817 other => Err(self.err(format!(
24818 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24819 ))),
24820 }
24821 }
24822
24823 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24824 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24825 /// `UNBOUNDED FOLLOWING`.
24826 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24827 // Interval-typed offset for a value-based RANGE frame over a
24828 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24829 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24830 // PRECEDING`.
24831 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24832 let dir = self.expect_ident_like()?;
24833 return if dir.eq_ignore_ascii_case("preceding") {
24834 Ok(FrameBound::IntervalPreceding {
24835 months,
24836 days,
24837 micros,
24838 })
24839 } else if dir.eq_ignore_ascii_case("following") {
24840 Ok(FrameBound::IntervalFollowing {
24841 months,
24842 days,
24843 micros,
24844 })
24845 } else {
24846 Err(self.err(format!(
24847 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24848 )))
24849 };
24850 }
24851 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24852 if let Token::Integer(n) = *self.peek() {
24853 self.advance();
24854 let n: u64 = u64::try_from(n).map_err(|_| {
24855 self.err(format!(
24856 "invalid frame offset {n} — expected non-negative integer"
24857 ))
24858 })?;
24859 let dir = self.expect_ident_like()?;
24860 return if dir.eq_ignore_ascii_case("preceding") {
24861 Ok(FrameBound::OffsetPreceding(n))
24862 } else if dir.eq_ignore_ascii_case("following") {
24863 Ok(FrameBound::OffsetFollowing(n))
24864 } else {
24865 Err(self.err(format!(
24866 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24867 )))
24868 };
24869 }
24870 let first = self.expect_ident_like()?;
24871 if first.eq_ignore_ascii_case("unbounded") {
24872 let dir = self.expect_ident_like()?;
24873 return if dir.eq_ignore_ascii_case("preceding") {
24874 Ok(FrameBound::UnboundedPreceding)
24875 } else if dir.eq_ignore_ascii_case("following") {
24876 Ok(FrameBound::UnboundedFollowing)
24877 } else {
24878 Err(self.err(format!(
24879 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24880 )))
24881 };
24882 }
24883 if first.eq_ignore_ascii_case("current") {
24884 let row = self.expect_ident_like()?;
24885 if !row.eq_ignore_ascii_case("row") {
24886 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24887 }
24888 return Ok(FrameBound::CurrentRow);
24889 }
24890 Err(self.err(format!(
24891 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24892 )))
24893 }
24894
24895 /// Detect and consume a leading interval offset in a frame bound —
24896 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24897 /// `(months, days, micros)`. Leaves the cursor on the trailing
24898 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24899 /// when the next tokens are not an interval offset.
24900 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24901 // Shape A — `INTERVAL '1 day'`.
24902 if matches!(self.peek(), Token::Interval) {
24903 self.advance(); // INTERVAL
24904 let atom = self.parse_interval_atom()?;
24905 if let Expr::Literal(Literal::Interval {
24906 months,
24907 days,
24908 micros,
24909 ..
24910 }) = atom
24911 {
24912 return Ok(Some((months, days, micros)));
24913 }
24914 return Err(self.err("expected an interval literal in frame offset".to_string()));
24915 }
24916 // Shape B — `'1 day'::interval`. Look ahead for the exact
24917 // string / `::` / interval-target triple before committing.
24918 if let Token::String(text) = self.peek() {
24919 let target_is_interval = match self.tokens.get(self.pos + 2) {
24920 Some(Token::Interval) => true,
24921 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24922 _ => false,
24923 };
24924 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24925 && target_is_interval;
24926 if is_cast {
24927 let text = text.clone();
24928 self.advance(); // string
24929 self.advance(); // ::
24930 self.advance(); // interval
24931 let parts = parse_interval_text(&text).ok_or_else(|| {
24932 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24933 })?;
24934 return Ok(Some(parts));
24935 }
24936 }
24937 Ok(None)
24938 }
24939
24940 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24941 if matches!(self.peek(), Token::Dot) {
24942 self.advance();
24943 let name = self.expect_ident_like()?;
24944 // v7.14.0 — schema-qualified function call
24945 // `<schema>.<fn>(args)`. PG dumps emit
24946 // `pg_catalog.set_config(...)` in the preamble. SPG
24947 // is single-namespace: drop the schema prefix and
24948 // route the dispatch on the bare function name.
24949 if matches!(self.peek(), Token::LParen) {
24950 return self.finish_ident_atom(name);
24951 }
24952 return Ok(Expr::Column(ColumnName {
24953 qualifier: Some(first),
24954 name,
24955 }));
24956 }
24957 if matches!(self.peek(), Token::LParen) {
24958 self.advance();
24959 // `COUNT(*)` — special-cased here because `*` isn't a normal
24960 // expression token. Lower-case match on `first` since the lexer
24961 // folds identifiers.
24962 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24963 self.advance();
24964 if !matches!(self.peek(), Token::RParen) {
24965 return Err(self.err(format!(
24966 "expected ')' after COUNT(*), got {:?}",
24967 self.peek()
24968 )));
24969 }
24970 self.advance();
24971 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24972 let filter = self.parse_filter_clause()?;
24973 // v4.12: COUNT(*) OVER (...) — same window tail.
24974 let null_treatment = self.parse_null_treatment_modifier();
24975 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24976 && s.eq_ignore_ascii_case("over")
24977 {
24978 self.advance();
24979 let (partition_by, order_by, frame) = self.parse_over_clause()?;
24980 return Ok(Expr::WindowFunction {
24981 name: "count_star".into(),
24982 args: Vec::new(),
24983 partition_by,
24984 order_by,
24985 frame,
24986 null_treatment,
24987 filter,
24988 });
24989 }
24990 if let Some(filter) = filter {
24991 return Ok(Expr::AggregateOrdered {
24992 call: Box::new(Expr::FunctionCall {
24993 name: "count_star".into(),
24994 args: Vec::new(),
24995 }),
24996 order_by: Vec::new(),
24997 distinct: false,
24998 filter: Some(filter),
24999 });
25000 }
25001 return Ok(Expr::FunctionCall {
25002 name: "count_star".into(),
25003 args: Vec::new(),
25004 });
25005 }
25006 // Function call. PG-style: zero-or-more comma-separated args.
25007 let mut args = Vec::new();
25008 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25009 // Names are collected in lock-step with `args` and resolved to
25010 // positional order after the loop (the AST stays positional).
25011 let mut arg_names: Vec<Option<String>> = Vec::new();
25012 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25013 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25014 // seen, so the value arguments before it can be folded.
25015 let mut saw_separator = false;
25016 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25017 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25018 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25019 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25020 self.advance();
25021 true
25022 } else if matches!(self.peek(), Token::All) {
25023 self.advance();
25024 false
25025 } else {
25026 false
25027 };
25028 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25029 // TIMESTAMPDIFF take a bare unit keyword as the first
25030 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25031 // bare type keyword (DATE / TIME / DATETIME); lower them
25032 // onto string literals so the evaluator sees plain text.
25033 if ((first.eq_ignore_ascii_case("timestampadd")
25034 || first.eq_ignore_ascii_case("timestampdiff"))
25035 && matches!(self.peek(), Token::Ident(u) if matches!(
25036 u.to_ascii_lowercase().as_str(),
25037 "microsecond" | "second" | "minute" | "hour" | "day"
25038 | "week" | "month" | "quarter" | "year"
25039 )))
25040 || (first.eq_ignore_ascii_case("get_format")
25041 && matches!(self.peek(), Token::Ident(u) if matches!(
25042 u.to_ascii_lowercase().as_str(),
25043 "date" | "time" | "datetime" | "timestamp"
25044 )))
25045 {
25046 if let Token::Ident(u) = self.peek() {
25047 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25048 }
25049 self.advance();
25050 if matches!(self.peek(), Token::Comma) {
25051 self.advance();
25052 }
25053 }
25054 // `ROW(a, b, …)` keyword constructor. Followed by a
25055 // comparison operator or [NOT] IN it joins the paren
25056 // row-constructor machinery (fieldwise parse-time
25057 // expansion); bare, it stays a `row` call the evaluator
25058 // renders as PG record text.
25059 if first.eq_ignore_ascii_case("row") {
25060 let mut row_items = Vec::new();
25061 if !matches!(self.peek(), Token::RParen) {
25062 loop {
25063 row_items.push(self.parse_expr(0)?);
25064 match self.peek() {
25065 Token::Comma => {
25066 self.advance();
25067 }
25068 Token::RParen => break,
25069 other => {
25070 return Err(self.err(format!(
25071 "expected ',' or ')' in ROW(...), got {other:?}"
25072 )));
25073 }
25074 }
25075 }
25076 }
25077 self.advance(); // ')'
25078 let comparison_follows = matches!(
25079 self.peek(),
25080 Token::Eq
25081 | Token::NotEq
25082 | Token::Lt
25083 | Token::LtEq
25084 | Token::Gt
25085 | Token::GtEq
25086 | Token::In
25087 ) || (matches!(self.peek(), Token::Not)
25088 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25089 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25090 if comparison_follows && !row_items.is_empty() {
25091 return self.parse_row_comparison_tail(row_items);
25092 }
25093 return Ok(Expr::FunctionCall {
25094 name: String::from("row"),
25095 args: row_items,
25096 });
25097 }
25098 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25099 // the parse-mode keyword introduces the source text. SPG
25100 // carries XML as text, so both modes lower to __xmlparse(expr)
25101 // which validates well-formedness and returns Value::Xml.
25102 if first.eq_ignore_ascii_case("xmlparse")
25103 && matches!(self.peek(), Token::Ident(kw)
25104 if kw.eq_ignore_ascii_case("document")
25105 || kw.eq_ignore_ascii_case("content"))
25106 {
25107 let mode = match self.advance() {
25108 Token::Ident(kw) => kw.to_ascii_lowercase(),
25109 _ => unreachable!("peeked an ident"),
25110 };
25111 let src = self.parse_expr(0)?;
25112 if !matches!(self.peek(), Token::RParen) {
25113 return Err(self.err(format!(
25114 "expected ')' to close XMLPARSE, got {:?}",
25115 self.peek()
25116 )));
25117 }
25118 self.advance();
25119 return Ok(Expr::FunctionCall {
25120 name: String::from("__xmlparse"),
25121 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25122 });
25123 }
25124 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25125 // keyword introduces the element name (a bare or quoted
25126 // identifier), then optional content expressions. Lower to a
25127 // plain `xmlelement(name_text, content …)` call.
25128 if first.eq_ignore_ascii_case("xmlelement")
25129 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25130 {
25131 self.advance(); // consume NAME
25132 let elem_name = match self.peek().clone() {
25133 Token::Ident(n) | Token::QuotedIdent(n) => {
25134 self.advance();
25135 n
25136 }
25137 other => {
25138 return Err(self.err(format!(
25139 "expected element name after XMLELEMENT NAME, got {other:?}"
25140 )));
25141 }
25142 };
25143 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25144 while matches!(self.peek(), Token::Comma) {
25145 self.advance();
25146 args.push(self.parse_expr(0)?);
25147 }
25148 if !matches!(self.peek(), Token::RParen) {
25149 return Err(self.err(format!(
25150 "expected ')' to close XMLELEMENT, got {:?}",
25151 self.peek()
25152 )));
25153 }
25154 self.advance();
25155 return Ok(Expr::FunctionCall {
25156 name: String::from("xmlelement"),
25157 args,
25158 });
25159 }
25160 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25161 // becomes a `<name>value</name>` element; a bare column infers its
25162 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25163 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25164 let mut args: Vec<Expr> = Vec::new();
25165 loop {
25166 let val = self.parse_expr(0)?;
25167 let name = if matches!(self.peek(), Token::As) {
25168 self.advance();
25169 match self.peek().clone() {
25170 Token::Ident(n) | Token::QuotedIdent(n) => {
25171 self.advance();
25172 n
25173 }
25174 other => {
25175 return Err(self.err(format!(
25176 "expected name after AS in XMLFOREST, got {other:?}"
25177 )));
25178 }
25179 }
25180 } else if let Expr::Column(c) = &val {
25181 c.name.clone()
25182 } else {
25183 return Err(
25184 self.err("XMLFOREST element without a column name needs AS".into())
25185 );
25186 };
25187 args.push(Expr::Literal(Literal::String(name)));
25188 args.push(val);
25189 if matches!(self.peek(), Token::Comma) {
25190 self.advance();
25191 } else {
25192 break;
25193 }
25194 }
25195 if !matches!(self.peek(), Token::RParen) {
25196 return Err(self.err(format!(
25197 "expected ')' to close XMLFOREST, got {:?}",
25198 self.peek()
25199 )));
25200 }
25201 self.advance();
25202 return Ok(Expr::FunctionCall {
25203 name: String::from("xmlforest"),
25204 args,
25205 });
25206 }
25207 // SQL-standard `POSITION(sub IN str)` — lowers onto
25208 // strpos(str, sub). IN is the argument separator here,
25209 // so the needle parses with the IN-tail suppressed.
25210 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25211 let saved = self.suppress_in_tail;
25212 self.suppress_in_tail = true;
25213 let needle = self.parse_expr(0);
25214 self.suppress_in_tail = saved;
25215 let needle = needle?;
25216 if matches!(self.peek(), Token::In) {
25217 self.advance();
25218 let haystack = self.parse_expr(0)?;
25219 if !matches!(self.peek(), Token::RParen) {
25220 return Err(self.err(format!(
25221 "expected ')' to close POSITION, got {:?}",
25222 self.peek()
25223 )));
25224 }
25225 self.advance();
25226 return Ok(Expr::FunctionCall {
25227 name: String::from("strpos"),
25228 args: alloc::vec![haystack, needle],
25229 });
25230 }
25231 // position(sub, str) comma form (incl. bytea) —
25232 // hand the parsed first arg to the generic list.
25233 args.push(needle);
25234 if matches!(self.peek(), Token::Comma) {
25235 self.advance();
25236 }
25237 }
25238 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25239 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25240 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25241 // riding the generic argument list below.
25242 if first.eq_ignore_ascii_case("trim") {
25243 let mode = match self.peek() {
25244 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25245 self.advance();
25246 Some("btrim")
25247 }
25248 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25249 self.advance();
25250 Some("ltrim")
25251 }
25252 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25253 self.advance();
25254 Some("rtrim")
25255 }
25256 _ => None,
25257 };
25258 if mode.is_some() || matches!(self.peek(), Token::From) {
25259 // TRIM([mode] FROM str) — no strip-chars.
25260 let chars = if matches!(self.peek(), Token::From) {
25261 None
25262 } else {
25263 Some(self.parse_expr(0)?)
25264 };
25265 if !matches!(self.peek(), Token::From) {
25266 return Err(self.err(format!(
25267 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25268 self.peek()
25269 )));
25270 }
25271 self.advance();
25272 let target = self.parse_expr(0)?;
25273 if !matches!(self.peek(), Token::RParen) {
25274 return Err(
25275 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25276 );
25277 }
25278 self.advance();
25279 let mut trim_args = alloc::vec![target];
25280 if let Some(c) = chars {
25281 trim_args.push(c);
25282 }
25283 return Ok(Expr::FunctionCall {
25284 name: String::from(mode.unwrap_or("btrim")),
25285 args: trim_args,
25286 });
25287 }
25288 }
25289 if !matches!(self.peek(), Token::RParen) {
25290 loop {
25291 // v7.38 (read01, T14) — `argname => value` names this arg.
25292 // v7.39 (read01 round 77) — `argname := value` is the same
25293 // thing, and it is the spelling PG's own docs lead with. It
25294 // was simply never lexed here, so every `f(x := 1)` died in
25295 // the parser regardless of what `f` was.
25296 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25297 (
25298 Token::Ident(n) | Token::QuotedIdent(n),
25299 Some(Token::FatArrow | Token::ColonEq),
25300 ) => {
25301 let name = n.clone();
25302 self.advance(); // name
25303 self.advance(); // => / :=
25304 Some(name)
25305 }
25306 _ => None,
25307 };
25308 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25309 // array's elements into a variadic call's trailing args
25310 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25311 // reserved, so it arrives as a bare ident before the arg.
25312 let is_variadic = this_name.is_none()
25313 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25314 if is_variadic {
25315 self.advance();
25316 }
25317 let arg = self.parse_expr(0)?;
25318 args.push(match &this_name {
25319 // The callee's parameter names decide the slot, and a
25320 // user function's live in the catalog. Carry the name
25321 // to eval rather than guessing here.
25322 Some(n) => Expr::NamedArg {
25323 name: n.clone(),
25324 expr: Box::new(arg),
25325 },
25326 None if is_variadic => Expr::Variadic(Box::new(arg)),
25327 None => arg,
25328 });
25329 arg_names.push(this_name);
25330 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25331 // The `::` cast already worked; this lowers the
25332 // function form onto the same Expr::Cast node.
25333 if first.eq_ignore_ascii_case("cast")
25334 && args.len() == 1
25335 && matches!(self.peek(), Token::As)
25336 {
25337 self.advance();
25338 let target = self.parse_cast_target()?;
25339 if !matches!(self.peek(), Token::RParen) {
25340 return Err(self.err(format!(
25341 "expected ')' to close CAST, got {:?}",
25342 self.peek()
25343 )));
25344 }
25345 self.advance();
25346 return Ok(Expr::Cast {
25347 expr: Box::new(args.pop().expect("one arg")),
25348 target,
25349 });
25350 }
25351 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25352 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25353 // keywords; SPG's lexer makes them plain idents (so they'd be
25354 // read as column refs). Lower the keyword to the string form
25355 // the evaluator already accepts.
25356 if first.eq_ignore_ascii_case("normalize")
25357 && args.len() == 1
25358 && matches!(self.peek(), Token::Comma)
25359 {
25360 let form = match self.tokens.get(self.pos + 1) {
25361 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25362 let up = f.to_ascii_uppercase();
25363 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25364 }
25365 _ => None,
25366 };
25367 if let Some(up) = form {
25368 self.advance(); // comma
25369 self.advance(); // form keyword
25370 args.push(Expr::Literal(Literal::String(up)));
25371 }
25372 }
25373 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25374 // form. Desugars to the comma-list shape evaluator already
25375 // handles. Triggered after the first arg when the function
25376 // name is substring / substr and the next token is FROM
25377 // (a reserved keyword in PG; SPG also reserves it).
25378 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25379 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25380 // internal __substring_similar(str, pat, esc) call.
25381 if (first.eq_ignore_ascii_case("substring")
25382 || first.eq_ignore_ascii_case("substr"))
25383 && args.len() == 1
25384 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25385 {
25386 self.advance(); // SIMILAR
25387 let pattern = self.parse_expr(0)?;
25388 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25389 {
25390 return Err(self.err(format!(
25391 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25392 self.peek()
25393 )));
25394 }
25395 self.advance(); // ESCAPE
25396 let esc = self.parse_expr(0)?;
25397 if !matches!(self.peek(), Token::RParen) {
25398 return Err(self.err(format!(
25399 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25400 self.peek()
25401 )));
25402 }
25403 self.advance();
25404 args.push(pattern);
25405 args.push(esc);
25406 return Ok(Expr::FunctionCall {
25407 name: "__substring_similar".to_string(),
25408 args,
25409 });
25410 }
25411 if (first.eq_ignore_ascii_case("substring")
25412 || first.eq_ignore_ascii_case("substr"))
25413 && args.len() == 1
25414 && matches!(self.peek(), Token::From | Token::For)
25415 {
25416 // `substring(str FROM pos [FOR len])`, or the FOR-only
25417 // `substring(str FOR len)` which PG treats as FROM 1.
25418 if matches!(self.peek(), Token::From) {
25419 self.advance();
25420 let start = self.parse_expr(0)?;
25421 args.push(start);
25422 } else {
25423 args.push(Expr::Literal(Literal::Integer(1)));
25424 }
25425 if matches!(self.peek(), Token::For) {
25426 self.advance();
25427 let length = self.parse_expr(0)?;
25428 args.push(length);
25429 }
25430 if !matches!(self.peek(), Token::RParen) {
25431 return Err(self.err(format!(
25432 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25433 self.peek()
25434 )));
25435 }
25436 self.advance();
25437 return Ok(Expr::FunctionCall {
25438 name: first.to_ascii_lowercase(),
25439 args,
25440 });
25441 }
25442 // PG `overlay(str PLACING repl FROM n [FOR len])`
25443 // syntactic form. Desugars to the `overlay(str,
25444 // repl, n[, len])` comma-list shape the evaluator
25445 // already implements. `PLACING` is not a reserved
25446 // token in SPG, so it arrives as a bare Ident.
25447 if first.eq_ignore_ascii_case("overlay")
25448 && args.len() == 1
25449 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25450 {
25451 self.advance(); // consume PLACING
25452 args.push(self.parse_expr(0)?); // replacement
25453 if !matches!(self.peek(), Token::From) {
25454 return Err(self.err(format!(
25455 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25456 self.peek()
25457 )));
25458 }
25459 self.advance();
25460 args.push(self.parse_expr(0)?); // start position
25461 if matches!(self.peek(), Token::For) {
25462 self.advance();
25463 args.push(self.parse_expr(0)?); // length
25464 }
25465 if !matches!(self.peek(), Token::RParen) {
25466 return Err(self.err(format!(
25467 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25468 self.peek()
25469 )));
25470 }
25471 self.advance();
25472 return Ok(Expr::FunctionCall {
25473 name: String::from("overlay"),
25474 args,
25475 });
25476 }
25477 // `TRIM(chars FROM str)` — the keyword-less
25478 // spelling lands here after the chars parse
25479 // (the keyword forms return earlier).
25480 if first.eq_ignore_ascii_case("trim")
25481 && args.len() == 1
25482 && matches!(self.peek(), Token::From)
25483 {
25484 self.advance();
25485 let target = self.parse_expr(0)?;
25486 if !matches!(self.peek(), Token::RParen) {
25487 return Err(self.err(format!(
25488 "expected ')' to close TRIM(chars FROM str), got {:?}",
25489 self.peek()
25490 )));
25491 }
25492 self.advance();
25493 let chars = args.pop().expect("one arg");
25494 return Ok(Expr::FunctionCall {
25495 name: String::from("btrim"),
25496 args: alloc::vec![target, chars],
25497 });
25498 }
25499 // v7.24 (round-16 A) — aggregate-internal
25500 // ordering: `array_agg(x ORDER BY y DESC NULLS
25501 // LAST)`. Keys close the argument list.
25502 if matches!(self.peek(), Token::Order) {
25503 self.advance();
25504 if !self.peek_is_by() {
25505 return Err(self.err(format!(
25506 "expected BY after ORDER in aggregate args, got {:?}",
25507 self.peek()
25508 )));
25509 }
25510 self.advance();
25511 loop {
25512 // v7.39 (round 691) — save/restore, the discipline this parser
25513 // already uses around `pending_sample_preds`, so a subquery inside
25514 // a key neither inherits nor leaks the channel.
25515 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25516 let saved_coll = self.order_key_collation.take();
25517 let parsed = self.parse_expr(0);
25518 self.in_order_by_key = saved_flag;
25519 let collation =
25520 core::mem::replace(&mut self.order_key_collation, saved_coll);
25521 let expr = parsed?;
25522 let desc = if matches!(self.peek(), Token::Desc) {
25523 self.advance();
25524 true
25525 } else if matches!(self.peek(), Token::Asc) {
25526 self.advance();
25527 false
25528 } else {
25529 false
25530 };
25531 let nulls_first = self.parse_optional_nulls_placement()?;
25532 agg_order_by.push(OrderBy {
25533 expr,
25534 desc,
25535 nulls_first,
25536 collation,
25537 });
25538 if matches!(self.peek(), Token::Comma) {
25539 self.advance();
25540 } else {
25541 break;
25542 }
25543 }
25544 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25545 // follow the ORDER BY inside GROUP_CONCAT.
25546 if self.consume_group_concat_separator(&mut args)? {
25547 saw_separator = true;
25548 }
25549 if !matches!(self.peek(), Token::RParen) {
25550 return Err(self.err(format!(
25551 "expected ')' after aggregate ORDER BY, got {:?}",
25552 self.peek()
25553 )));
25554 }
25555 break;
25556 }
25557 // v7.39 (round 354, M12) — …or directly after the
25558 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25559 // own spelling of what PG passes as string_agg's second
25560 // argument; it was a parse error, so every MySQL query
25561 // that names its own separator failed outright.
25562 if self.consume_group_concat_separator(&mut args)? {
25563 saw_separator = true;
25564 break;
25565 }
25566 match self.peek() {
25567 Token::Comma => {
25568 self.advance();
25569 }
25570 Token::RParen => break,
25571 other => {
25572 return Err(self.err(format!(
25573 "expected ',' or ')' in function args, got {other:?}"
25574 )));
25575 }
25576 }
25577 }
25578 }
25579 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25580 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25581 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25582 // meaning a separator — that is what the explicit SEPARATOR
25583 // tail is for. Fold them into one `concat(...)` so the
25584 // aggregate keeps its single value argument.
25585 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25586 let values = args.len() - usize::from(saw_separator);
25587 if values > 1 {
25588 let sep_arg = if saw_separator { args.pop() } else { None };
25589 let folded = Expr::FunctionCall {
25590 name: "concat".to_string(),
25591 args: core::mem::take(&mut args),
25592 };
25593 args.push(folded);
25594 if let Some(sep) = sep_arg {
25595 args.push(sep);
25596 }
25597 }
25598 }
25599 self.advance(); // consume ')'
25600 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25601 // any more. The parser has no catalog, so it could only ever resolve
25602 // the handful of `make_*` builtins whose parameter names were baked
25603 // into a table right here — every user function got
25604 // "does not support named arguments", though the catalog has been
25605 // storing its parameter names all along. Reordering happens in eval,
25606 // in one place, for builtins and user functions alike.
25607 // v7.32 (round-29) — ordered-set aggregate tail
25608 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25609 // (percentile_cont / percentile_disc / mode). The sort spec
25610 // lands in the same `order_by` slot a decorated aggregate
25611 // uses; the executor dispatches on the function name. WITHIN
25612 // GROUP and an intra-argument ORDER BY are mutually
25613 // exclusive (PG rejects both).
25614 let within_group_order = self.parse_within_group_clause()?;
25615 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25616 return Err(self.err(
25617 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25618 .into(),
25619 ));
25620 }
25621 let within_group_seen = !within_group_order.is_empty();
25622 let agg_order_by = if within_group_order.is_empty() {
25623 agg_order_by
25624 } else {
25625 within_group_order
25626 };
25627 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25628 let filter = self.parse_filter_clause()?;
25629 // v4.12: window-function tail — `name(args) OVER (...)`.
25630 // Promotes the just-parsed FunctionCall into a
25631 // WindowFunction node carrying partition + order.
25632 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25633 // / `RESPECT NULLS OVER (...)` between the closing paren
25634 // and `OVER`.
25635 let null_treatment = self.parse_null_treatment_modifier();
25636 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25637 && s.eq_ignore_ascii_case("over")
25638 {
25639 self.advance();
25640 // v7.39 (round 230) — PG implements neither modifier for a
25641 // windowed call and says so (0A000). Both used to be parsed
25642 // and then silently dropped here, so `count(DISTINCT v)
25643 // OVER (…)` quietly answered the non-distinct count.
25644 if agg_distinct {
25645 return Err(
25646 self.err("DISTINCT is not implemented for window functions".to_string())
25647 );
25648 }
25649 if !agg_order_by.is_empty() {
25650 // PG separates the two shapes that land here: a
25651 // WITHIN GROUP call is an ordered-set aggregate and gets
25652 // its own message naming the aggregate; a plain
25653 // `agg(x ORDER BY y)` gets the generic one.
25654 let msg = if within_group_seen {
25655 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25656 } else {
25657 "aggregate ORDER BY is not implemented for window functions".to_string()
25658 };
25659 return Err(self.err(msg));
25660 }
25661 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25662 return Ok(Expr::WindowFunction {
25663 name: first,
25664 args,
25665 partition_by,
25666 order_by,
25667 frame,
25668 null_treatment,
25669 filter,
25670 });
25671 }
25672 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25673 return Ok(Expr::AggregateOrdered {
25674 call: Box::new(Expr::FunctionCall { name: first, args }),
25675 order_by: agg_order_by,
25676 distinct: agg_distinct,
25677 filter,
25678 });
25679 }
25680 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25681 // over TIMESTAMPTZ and has no timestamp overload, so a
25682 // timestamp argument is coerced on the way in and the answer
25683 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25684 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25685 // zone`. SPG answered `timestamp without time zone`, dropping
25686 // the offset from every rendering.
25687 //
25688 // Writing the coercion PG performs makes the existing
25689 // argument-driven typing (the one `date_trunc` uses) reach the
25690 // right answer, rather than teaching the type layer a second
25691 // rule. MySQL's DATE_ADD is a different function that returns
25692 // DATE or DATETIME, so this is PG-dialect only.
25693 //
25694 // Out-of-line because this sits on the RECURSIVE descent
25695 // frame: an inline block with locals here costs every nesting
25696 // level, and the suite's deep-nesting sentinel overflowed the
25697 // 512 KiB parser stack the moment one was added (round 430's
25698 // lesson, in the same shape).
25699 if !self.mysql_dialect {
25700 lift_date_add_arg_to_timestamptz(&first, &mut args);
25701 }
25702 return Ok(Expr::FunctionCall { name: first, args });
25703 }
25704 // v7.9.20 — SQL-standard parenless keyword expressions
25705 // (PG treats these as functions called without parens).
25706 // Resolve to a synthetic FunctionCall so the engine's
25707 // eval path reuses the existing function-call routing.
25708 // mailrs G3.
25709 let lc = first.to_ascii_lowercase();
25710 if matches!(
25711 lc.as_str(),
25712 "current_date"
25713 | "current_time"
25714 | "current_timestamp"
25715 | "localtimestamp"
25716 | "localtime"
25717 // v7.37.17 (17.6 siblings) — session-identity SQL-
25718 // standard parenless keywords. current_user /
25719 // session_user / user were already caught by the
25720 // pgwire canned-response shortcut but bare-select
25721 // in the embedded engine went through Expr::Column
25722 // and errored. Adding them here so the parser
25723 // resolves to a synthetic FunctionCall that reuses
25724 // the existing eval/functions.rs dispatch.
25725 | "current_user"
25726 | "session_user"
25727 | "current_role"
25728 | "current_catalog"
25729 | "current_schema"
25730 | "current_database"
25731 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25732 | "system_user"
25733 ) {
25734 return Ok(Expr::FunctionCall {
25735 name: lc,
25736 args: Vec::new(),
25737 });
25738 }
25739 Ok(Expr::Column(ColumnName {
25740 qualifier: None,
25741 name: first,
25742 }))
25743 }
25744}
25745
25746/// v7.39 (round 522) — write the coercion PG's `date_add` /
25747/// `date_subtract` signature performs.
25748///
25749/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25750/// timestamp argument is cast on the way in and the answer is
25751/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25752/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25753/// `timestamp without time zone`, dropping the offset from every
25754/// rendering of the result.
25755///
25756/// Writing the cast the signature implies lets the existing
25757/// argument-driven typing (the one `date_trunc` uses) reach the right
25758/// answer instead of teaching the type layer a second rule. MySQL's
25759/// DATE_ADD is a different function returning DATE or DATETIME, so the
25760/// caller applies this in PG dialect only.
25761///
25762/// A free function, and not a block at the call site, because the caller
25763/// is on the recursive-descent frame chain.
25764#[inline(never)]
25765fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25766 if args.len() != 2
25767 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25768 {
25769 return;
25770 }
25771 let base = args.remove(0);
25772 args.insert(
25773 0,
25774 Expr::Cast {
25775 expr: Box::new(base),
25776 target: CastTarget::Timestamptz,
25777 },
25778 );
25779}
25780
25781/// v6.8.2 — walk an expression tree and return the first column
25782/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25783/// to derive `CreateIndexStatement.column` from an expression
25784/// key (so downstream planner code resolving a primary column
25785/// position keeps working with expression indexes). Returns
25786/// `None` when the expression has no column ref at all — caller
25787/// surfaces that as a parse error.
25788fn extract_first_column(expr: &Expr) -> Option<String> {
25789 match expr {
25790 Expr::Column(cn) => Some(cn.name.clone()),
25791 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25792 Expr::Binary { lhs, rhs, .. } => {
25793 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25794 }
25795 Expr::Unary { expr: e, .. } => extract_first_column(e),
25796 // v7.39 (read01 round 93) — a cast wraps its operand: a common
25797 // expression-index key is `lower(col::text)`, where the column
25798 // sits under the `::text` cast inside the function arg. Without
25799 // descending here the key was rejected as "references no column".
25800 Expr::Cast { expr: e, .. } => extract_first_column(e),
25801 _ => None,
25802 }
25803}
25804
25805fn maybe_not(expr: Expr, negated: bool) -> Expr {
25806 if negated {
25807 Expr::Unary {
25808 op: UnOp::Not,
25809 expr: Box::new(expr),
25810 }
25811 } else {
25812 expr
25813 }
25814}
25815
25816/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25817/// things in the two dialects, and SPG read all three PG's way:
25818///
25819/// | token | PG (and SPG) | MySQL, measured |
25820/// |---|---|---|
25821/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25822/// | `&&` | inet / array overlap | **AND** |
25823/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25824///
25825/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25826/// answer with no error, which is why they are routed here rather than
25827/// left to the shared table.
25828impl Parser {
25829 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25830 if self.mysql_dialect {
25831 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25832 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25833 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25834 if let Token::Ident(w) = tok
25835 && w.eq_ignore_ascii_case("div")
25836 {
25837 return Some((BinOp::IntDiv, 8));
25838 }
25839 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25840 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25841 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25842 // there sits in operand position, not infix).
25843 if let Token::Ident(w) = tok
25844 && w.eq_ignore_ascii_case("mod")
25845 {
25846 return Some((BinOp::Mod, 8));
25847 }
25848 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25849 // plain ident to the lexer. Its precedence sits between OR (1)
25850 // and AND (3) — hence rung 2, the slot freed by moving AND up.
25851 if let Token::Ident(w) = tok
25852 && w.eq_ignore_ascii_case("xor")
25853 {
25854 return Some((BinOp::LogicalXor, 2));
25855 }
25856 match tok {
25857 Token::Concat => return Some((BinOp::Or, 1)),
25858 // MySQL's `&&` is logical AND, sharing AND's rung (3).
25859 Token::InetOverlap => return Some((BinOp::And, 3)),
25860 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25861 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25862 _ => {}
25863 }
25864 }
25865 binop_from(tok)
25866 }
25867}
25868
25869// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25870// (which sits strictly between OR and AND), every level from AND upward was
25871// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25872// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25873// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25874// the *relative* order of every PG operator is unchanged by the shift.
25875fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25876 let pair = match tok {
25877 Token::Or => (BinOp::Or, 1),
25878 Token::And => (BinOp::And, 3),
25879 Token::Eq => (BinOp::Eq, 5),
25880 Token::NotEq => (BinOp::NotEq, 5),
25881 Token::Lt => (BinOp::Lt, 5),
25882 Token::LtEq => (BinOp::LtEq, 5),
25883 Token::Gt => (BinOp::Gt, 5),
25884 Token::GtEq => (BinOp::GtEq, 5),
25885 // pgvector distance ops all sit on the same rung — tighter than
25886 // comparisons (5) so `col <-> v < threshold` parses correctly.
25887 Token::L2Distance => (BinOp::L2Distance, 6),
25888 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25889 // comparison rung.
25890 Token::GeomParallel => (BinOp::GeomParallel, 5),
25891 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25892 // comparison rung.
25893 Token::OverLeft => (BinOp::OverLeft, 5),
25894 Token::OverRight => (BinOp::OverRight, 5),
25895 Token::GeomPerp => (BinOp::GeomPerp, 5),
25896 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25897 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25898 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25899 Token::InnerProduct => (BinOp::InnerProduct, 6),
25900 Token::CosineDistance => (BinOp::CosineDistance, 6),
25901 Token::Plus => (BinOp::Add, 7),
25902 Token::Minus => (BinOp::Sub, 7),
25903 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25904 // binds every "other" operator (`||`, `|`, `&`, `#`, the
25905 // pgvector distances above) BETWEEN additive (7) and the
25906 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25907 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25908 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25909 // ("matches PG conceptually" — the round-753 audit measured it
25910 // false; the old rung errored on `'a' || 1 + 1` with
25911 // `text + integer`). Same-level chains left-fold, as PG does.
25912 Token::Concat => (BinOp::Concat, 6),
25913 Token::Pipe => (BinOp::BitOr, 6),
25914 Token::Amp => (BinOp::BitAnd, 6),
25915 Token::Star => (BinOp::Mul, 8),
25916 Token::Slash => (BinOp::Div, 8),
25917 Token::Percent => (BinOp::Mod, 8),
25918 // v4.14: JSON path ops bind tighter than comparisons (5)
25919 // and additive (7) so `doc->'k' = 'v'` parses correctly.
25920 // Same rung as the multiplicative ops.
25921 Token::JsonGet => (BinOp::JsonGet, 8),
25922 Token::JsonGetText => (BinOp::JsonGetText, 8),
25923 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25924 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25925 Token::JsonContains => (BinOp::JsonContains, 8),
25926 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25927 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25928 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25929 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25930 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25931 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25932 // v7.12.2 — `@@` binds at the comparison rung (looser than
25933 // arithmetic, tighter than AND / OR). PG places `@@` at
25934 // the same precedence as `=` / `<`, so we follow.
25935 Token::TsMatch => (BinOp::TsMatch, 5),
25936 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25937 // PG places these at the comparison rung (same level as `=`),
25938 // so we follow.
25939 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25940 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25941 Token::InetContains => (BinOp::InetContains, 5),
25942 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25943 Token::InetOverlap => (BinOp::InetOverlap, 5),
25944 // v7.39 (round 508) — the geometric and pattern-order predicates
25945 // ride the comparison rung, as every other predicate does.
25946 Token::Intersects => (BinOp::Intersects, 5),
25947 Token::IsBelow => (BinOp::IsBelow, 5),
25948 Token::IsAbove => (BinOp::IsAbove, 5),
25949 Token::PatternLt => (BinOp::PatternLt, 5),
25950 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25951 Token::PatternGt => (BinOp::PatternGt, 5),
25952 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25953 // `@@@` is the old spelling of `@@` and means exactly it.
25954 Token::TsMatchOld => (BinOp::TsMatch, 5),
25955 _ => return None,
25956 };
25957 Some(pair)
25958}
25959
25960#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25961// `as f32` here is intentional: vector elements widen / narrow into f32 on
25962// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25963// past ~15 decimal digits — both are acceptable for a fixed-precision
25964// pgvector column.
25965/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25966/// implicit table alias and break trailing clauses. WITH lands
25967/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25968/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25969/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25970/// / VALUES / FOR / LATERAL — all of which would otherwise be
25971/// silently swallowed by `parse_optional_alias`.
25972fn is_alias_stopword(s: &str) -> bool {
25973 matches!(
25974 s.to_ascii_lowercase().as_str(),
25975 "with"
25976 | "on"
25977 | "where"
25978 | "having"
25979 | "group"
25980 | "order"
25981 | "limit"
25982 | "offset"
25983 | "union"
25984 | "except"
25985 | "intersect"
25986 | "returning"
25987 | "set"
25988 | "values"
25989 | "for"
25990 | "window"
25991 | "tablesample"
25992 | "lateral"
25993 | "left"
25994 | "right"
25995 | "inner"
25996 | "outer"
25997 | "full"
25998 | "cross"
25999 | "join"
26000 | "natural"
26001 | "using"
26002 | "fetch"
26003 )
26004}
26005
26006fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26007 match e {
26008 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26009 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26010 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26011 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26012 // so scale the divisor by hand instead of `f32::powi`.)
26013 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26014 let mut div = 1.0f32;
26015 for _ in 0..*scale {
26016 div *= 10.0;
26017 }
26018 Some(*unscaled as f32 / div)
26019 }
26020 Expr::Unary {
26021 op: UnOp::Neg,
26022 expr,
26023 } => extract_numeric_literal(expr).map(|x| -x),
26024 _ => None,
26025 }
26026}
26027
26028/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26029/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26030/// negative. Returns `None` if any pair fails to parse or no pair is found.
26031///
26032/// Recognised units (case-insensitive, optional trailing `s`):
26033/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26034/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26035/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26036/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26037/// (PG-canonical: DST and month-boundary semantics depend on this).
26038/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26039/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26040/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26041#[allow(clippy::cast_possible_truncation)]
26042fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26043 let mut months: i64 = 0;
26044 let mut days: i64 = 0;
26045 let mut micros: i64 = 0;
26046 let mut in_time = false;
26047 let mut num = alloc::string::String::new();
26048 for ch in rest.chars() {
26049 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26050 num.push(ch);
26051 continue;
26052 }
26053 if ch == 'T' || ch == 't' {
26054 if !num.is_empty() {
26055 return None;
26056 }
26057 in_time = true;
26058 continue;
26059 }
26060 let n: f64 = num.parse().ok()?;
26061 num.clear();
26062 match (ch, in_time) {
26063 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26064 ('M', false) => months += n as i64,
26065 ('W' | 'w', false) => days += (n * 7.0) as i64,
26066 ('D' | 'd', false) => days += n as i64,
26067 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26068 ('M', true) => micros += (n * 60_000_000.0) as i64,
26069 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26070 _ => return None,
26071 }
26072 }
26073 if !num.is_empty() {
26074 return None;
26075 }
26076 Some((
26077 i32::try_from(months).ok()?,
26078 i32::try_from(days).ok()?,
26079 micros,
26080 ))
26081}
26082
26083/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26084/// leading `-` negates the whole value). Rejects date-like strings.
26085fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26086 let (neg, body) = match s.strip_prefix('-') {
26087 Some(b) => (true, b),
26088 None => (false, s),
26089 };
26090 let (y, m) = body.split_once('-')?;
26091 let years: i32 = y.parse().ok()?;
26092 let mons: i32 = m.parse().ok()?;
26093 if years < 0 || mons < 0 {
26094 return None;
26095 }
26096 let total = years.checked_mul(12)?.checked_add(mons)?;
26097 Some((if neg { -total } else { total }, 0, 0))
26098}
26099
26100/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26101/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26102fn parse_interval_clock(tok: &str) -> Option<i64> {
26103 let (neg, body) = match tok.strip_prefix('-') {
26104 Some(r) => (true, r),
26105 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26106 };
26107 let mut it = body.split(':');
26108 let h: i64 = it.next()?.parse().ok()?;
26109 let m: i64 = it.next()?.parse().ok()?;
26110 let s_tok = it.next().unwrap_or("0");
26111 if it.next().is_some() {
26112 return None;
26113 }
26114 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26115 let sec: i64 = sec.parse().ok()?;
26116 let mut f = alloc::string::String::from(frac);
26117 while f.len() < 6 {
26118 f.push('0');
26119 }
26120 f.truncate(6);
26121 let fus: i64 = f.parse().ok()?;
26122 sec.checked_mul(1_000_000)?.checked_add(fus)?
26123 } else {
26124 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26125 };
26126 let total = h
26127 .checked_mul(3_600_000_000)?
26128 .checked_add(m.checked_mul(60_000_000)?)?
26129 .checked_add(sec_us)?;
26130 Some(if neg { -total } else { total })
26131}
26132
26133/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26134/// every spelling PG accepts (measured against live PG18.4, not guessed):
26135/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26136/// Before this, the unit table matched long names only, with an ad-hoc
26137/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26138/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26139/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26140/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26141/// fractional) both read from this one table now.
26142fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26143 let u = raw.to_ascii_lowercase();
26144 Some(match u.as_str() {
26145 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26146 "microsecond"
26147 }
26148 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26149 "millisecond"
26150 }
26151 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26152 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26153 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26154 "day" | "days" | "d" => "day",
26155 "week" | "weeks" | "w" => "week",
26156 "month" | "months" | "mon" | "mons" => "month",
26157 "year" | "years" | "yr" | "yrs" | "y" => "year",
26158 "decade" | "decades" | "dec" | "decs" => "decade",
26159 "century" | "centuries" | "cent" | "c" => "century",
26160 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26161 _ => return None,
26162 })
26163}
26164
26165/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26166/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26168pub(crate) enum IntervalField {
26169 Year,
26170 Month,
26171 Day,
26172 Hour,
26173 Minute,
26174 Second,
26175}
26176
26177/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26178/// spellings aren't standard for the qualifier position, so only the singular
26179/// forms are accepted.
26180/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26181/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26182/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26183/// take a `'1 2'` style literal — are not read here; they stay a parse
26184/// error rather than being silently misread.)
26185/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26186///
26187/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26188/// to do with a `@@` engine setting, and an unset one reads NULL rather
26189/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26190/// were the same node and `SELECT @x` answered "Unknown system variable".)
26191/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26192/// not see a session override — measured, after `SET autocommit=0`,
26193/// `@@global.autocommit` is still 1.
26194///
26195/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26196/// the parser's nesting budget is tuned against, and building these
26197/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26198/// wall `parse_left_right_atom` and friends were factored out for).
26199#[inline(never)]
26200fn variable_ref_atom(raw: &str) -> Expr {
26201 let user_var = !raw.starts_with("@@");
26202 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26203 Expr::FunctionCall {
26204 name: String::from(if user_var {
26205 "__spg_user_var"
26206 } else {
26207 "__spg_session_var"
26208 }),
26209 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26210 }
26211}
26212
26213fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26214 let Token::Ident(s) = tok else { return None };
26215 Some(match () {
26216 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26217 () if s.eq_ignore_ascii_case("second") => "second",
26218 () if s.eq_ignore_ascii_case("minute") => "minute",
26219 () if s.eq_ignore_ascii_case("hour") => "hour",
26220 () if s.eq_ignore_ascii_case("day") => "day",
26221 () if s.eq_ignore_ascii_case("week") => "week",
26222 () if s.eq_ignore_ascii_case("month") => "month",
26223 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26224 () if s.eq_ignore_ascii_case("year") => "year",
26225 () => return None,
26226 })
26227}
26228
26229/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26230/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26231/// which constructs the value at run time. Only the slot the unit names
26232/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26233/// slot the builtin has (months and fractional seconds respectively).
26234fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26235 let zero = || Expr::Literal(Literal::Integer(0));
26236 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26237 lhs: alloc::boxed::Box::new(qty.clone()),
26238 op,
26239 rhs: alloc::boxed::Box::new(by),
26240 };
26241 // (years, months, weeks, days, hours, mins, secs)
26242 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26243 match unit {
26244 "year" => args[0] = qty,
26245 "quarter" => {
26246 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26247 }
26248 "month" => args[1] = qty,
26249 "week" => args[2] = qty,
26250 "day" => args[3] = qty,
26251 "hour" => args[4] = qty,
26252 "minute" => args[5] = qty,
26253 "second" => args[6] = qty,
26254 // The builtin's seconds slot takes a fraction, so microseconds ride
26255 // it scaled down; the divisor is a NUMERIC literal so the division
26256 // stays exact rather than going through a float.
26257 "microsecond" => {
26258 args[6] = scaled(
26259 crate::ast::BinOp::Div,
26260 Expr::Literal(Literal::Numeric {
26261 unscaled: 1_000_000,
26262 scale: 0,
26263 }),
26264 );
26265 }
26266 _ => args[3] = qty,
26267 }
26268 Expr::FunctionCall {
26269 name: alloc::string::String::from("make_interval"),
26270 args,
26271 }
26272}
26273
26274/// `(count, unit)` → `(months, days, micros)`.
26275fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26276 let n: i64 = count.trim().parse().ok()?;
26277 Some(match unit {
26278 "microsecond" => (0, 0, n),
26279 "second" => (0, 0, n.checked_mul(1_000_000)?),
26280 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26281 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26282 "day" => (0, i32::try_from(n).ok()?, 0),
26283 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26284 "month" => (i32::try_from(n).ok()?, 0, 0),
26285 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26286 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26287 _ => return None,
26288 })
26289}
26290
26291fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26292 let Token::Ident(s) = tok else { return None };
26293 Some(match () {
26294 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26295 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26296 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26297 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26298 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26299 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26300 () => return None,
26301 })
26302}
26303
26304/// v7.39 (read01 round 102) — interpret an interval literal under a field
26305/// qualifier. Returns `(months, days, micros)`.
26306///
26307/// * A single field applied to a bare number sets which unit the number means,
26308/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26309/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26310/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26311/// * Every other range, and any literal a single field can't read as a plain
26312/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26313/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26314/// like PG, and the qualifier there only bounds precision.
26315fn interpret_qualified_interval(
26316 text: &str,
26317 (f1, f2): (IntervalField, Option<IntervalField>),
26318) -> Option<(i32, i32, i64)> {
26319 if let Some(f2) = f2 {
26320 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26321 if let Some(m) = parse_year_month_literal(text) {
26322 return Some((m, 0, 0));
26323 }
26324 }
26325 return parse_interval_text(text);
26326 }
26327 // Single field: reinterpret a bare number; otherwise the default parse.
26328 let trimmed = text.trim();
26329 if let Ok(val) = trimmed.parse::<f64>() {
26330 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26331 #[allow(clippy::cast_possible_truncation)]
26332 let whole = val as i64;
26333 #[allow(clippy::cast_possible_truncation)]
26334 let secs_micros = {
26335 let m = val * 1_000_000.0;
26336 if m >= 0.0 {
26337 (m + 0.5) as i64
26338 } else {
26339 (m - 0.5) as i64
26340 }
26341 };
26342 return Some(match f1 {
26343 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26344 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26345 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26346 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26347 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26348 IntervalField::Second => (0, 0, secs_micros),
26349 });
26350 }
26351 parse_interval_text(text)
26352}
26353
26354/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26355fn parse_year_month_literal(text: &str) -> Option<i32> {
26356 let t = text.trim();
26357 let (neg, body) = match t.strip_prefix('-') {
26358 Some(r) => (true, r),
26359 None => (false, t.strip_prefix('+').unwrap_or(t)),
26360 };
26361 let mut it = body.split('-');
26362 let years: i32 = it.next()?.trim().parse().ok()?;
26363 let months: i32 = match it.next() {
26364 Some(m) => m.trim().parse().ok()?,
26365 None => 0,
26366 };
26367 if it.next().is_some() {
26368 return None;
26369 }
26370 let total = years.checked_mul(12)?.checked_add(months)?;
26371 Some(if neg { -total } else { total })
26372}
26373
26374pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26375 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26376 // `@` is decorative; a trailing `ago` negates the whole interval.
26377 let mut trimmed = s.trim();
26378 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26379 let mut negate = false;
26380 if let Some(rest) = trimmed
26381 .strip_suffix("ago")
26382 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26383 {
26384 negate = true;
26385 trimmed = rest.trim();
26386 }
26387 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26388 let (mo, d, us) = v?;
26389 if negate {
26390 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26391 } else {
26392 Some((mo, d, us))
26393 }
26394 };
26395 let s = trimmed;
26396 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26397 // are single tokens, not the `<n> <unit>` pair form handled below.
26398 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26399 return finish(parse_iso8601_interval(rest));
26400 }
26401 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26402 if let Some(iv) = parse_year_month_interval(trimmed) {
26403 return finish(Some(iv));
26404 }
26405 }
26406 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26407 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26408 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26409 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26410 if let Ok(n) = trimmed.parse::<i64>() {
26411 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26412 }
26413 if let Ok(f) = trimmed.parse::<f64>() {
26414 if f.is_finite() {
26415 #[allow(clippy::cast_possible_truncation)]
26416 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26417 }
26418 }
26419 }
26420 // v7.39 (round 243) — PG accepts the number and unit run together
26421 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26422 // the `<n> <unit>` pair loop below sees them as two.
26423 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26424 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26425 for p in raw_parts {
26426 let boundary = p
26427 .char_indices()
26428 .find(|(i, c)| {
26429 *i > 0
26430 && c.is_ascii_alphabetic()
26431 && p[..*i]
26432 .chars()
26433 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26434 && p[..*i].chars().any(|d| d.is_ascii_digit())
26435 })
26436 .map(|(i, _)| i);
26437 match boundary {
26438 Some(i) => {
26439 parts.push(&p[..i]);
26440 parts.push(&p[i..]);
26441 }
26442 None => parts.push(p),
26443 }
26444 }
26445 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26446 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26447 // remains is the `<n> <unit>` pair form handled below.
26448 let mut clock_us: i64 = 0;
26449 let mut had_clock = false;
26450 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26451 clock_us = parse_interval_clock(parts[pos])?;
26452 parts.remove(pos);
26453 had_clock = true;
26454 }
26455 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26456 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26457 let mut lone_days: i32 = 0;
26458 if had_clock && parts.len() == 1 {
26459 if let Ok(n) = parts[0].parse::<i64>() {
26460 lone_days = i32::try_from(n).ok()?;
26461 parts.clear();
26462 }
26463 }
26464 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26465 return None;
26466 }
26467 let mut months: i32 = 0;
26468 let mut days: i32 = lone_days;
26469 let mut micros: i64 = clock_us;
26470 let mut i = 0;
26471 while i < parts.len() {
26472 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26473 if let Ok(n) = parts[i].parse::<i64>() {
26474 match unit_stripped {
26475 "microsecond" => micros = micros.checked_add(n)?,
26476 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26477 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26478 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26479 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26480 "day" => {
26481 let n32 = i32::try_from(n).ok()?;
26482 days = days.checked_add(n32)?;
26483 }
26484 "week" => {
26485 let n32 = i32::try_from(n).ok()?;
26486 days = days.checked_add(n32.checked_mul(7)?)?;
26487 }
26488 "month" => {
26489 let n32 = i32::try_from(n).ok()?;
26490 months = months.checked_add(n32)?;
26491 }
26492 "year" => {
26493 let n32 = i32::try_from(n).ok()?;
26494 months = months.checked_add(n32.checked_mul(12)?)?;
26495 }
26496 // v7.39 (read01 timestamp.c) — the larger calendar units.
26497 "decade" => {
26498 let n32 = i32::try_from(n).ok()?;
26499 months = months.checked_add(n32.checked_mul(120)?)?;
26500 }
26501 "century" => {
26502 let n32 = i32::try_from(n).ok()?;
26503 months = months.checked_add(n32.checked_mul(1200)?)?;
26504 }
26505 "millennium" => {
26506 let n32 = i32::try_from(n).ok()?;
26507 months = months.checked_add(n32.checked_mul(12000)?)?;
26508 }
26509 _ => return None,
26510 }
26511 } else if let Ok(f) = parts[i].parse::<f64>() {
26512 // Fractional units cascade down to the next-finer field the way
26513 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26514 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26515 // no_std: f64 has no trunc/fract/round methods, so do them with
26516 // casts (toward-zero) + explicit round-half-away-from-zero.
26517 #[allow(clippy::cast_possible_truncation)]
26518 fn round_i64(x: f64) -> i64 {
26519 if x >= 0.0 {
26520 (x + 0.5) as i64
26521 } else {
26522 (x - 0.5) as i64
26523 }
26524 }
26525 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26526 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26527 const DAY_US: f64 = 86_400_000_000.0;
26528 let whole = d as i64; // truncates toward zero
26529 let frac = d - whole as f64;
26530 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26531 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26532 Some(())
26533 }
26534 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26535 match unit_stripped {
26536 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26537 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26538 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26539 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26540 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26541 "day" => add_days_frac(&mut days, &mut micros, f)?,
26542 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26543 "month" => {
26544 let whole = f as i64;
26545 months = months.checked_add(i32::try_from(whole).ok()?)?;
26546 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26547 }
26548 "year" => {
26549 let m = f * 12.0;
26550 let whole = m as i64;
26551 months = months.checked_add(i32::try_from(whole).ok()?)?;
26552 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26553 }
26554 _ => return None,
26555 }
26556 } else {
26557 return None;
26558 }
26559 i += 2;
26560 }
26561 finish(Some((months, days, micros)))
26562}
26563
26564/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26565/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26566/// `interval` is intentionally absent (handled by its own parser arm).
26567/// Returns `None` for names that aren't sensible as a bare typed literal, so
26568/// the caller falls back to treating the ident as a column reference.
26569fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26570 Some(match ident {
26571 "date" => CastTarget::Date,
26572 "timestamp" | "datetime" => CastTarget::Timestamp,
26573 "timestamptz" => CastTarget::Timestamptz,
26574 "bool" | "boolean" => CastTarget::Bool,
26575 "int" | "integer" | "int4" => CastTarget::Int,
26576 "bigint" | "int8" => CastTarget::BigInt,
26577 "float8" | "double precision" => CastTarget::Float,
26578 "uuid" => CastTarget::Uuid,
26579 "bytea" => CastTarget::Bytea,
26580 "json" => CastTarget::Json,
26581 "jsonb" => CastTarget::Jsonb,
26582 // Types without a dedicated CastTarget variant flow through the
26583 // generic Named path (engine resolves via column_type_to_data_type).
26584 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26585 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26586 | "money" | "bit" | "varbit"
26587 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26588 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26589 // Range / multirange types likewise.
26590 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26591 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26592 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26593 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26594 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26595 CastTarget::Named(alloc::string::String::from(ident))
26596 }
26597 _ => return None,
26598 })
26599}
26600
26601/// v7.12.4 — map a bare type-name identifier (the form that
26602/// appears in a function arg list or RETURNS clause) to a
26603/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26604/// types so the caller can preserve them as
26605/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26606///
26607/// Subset of the full column-type grammar — we deliberately
26608/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26609/// here because function-arg types in v7.12.4 are mostly the
26610/// bare form (`text`, `int`, `bytea`, …).
26611/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26612/// than being `name TYPE`?
26613///
26614/// The multi-word spellings SQL allows for a bare argument type, each
26615/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26616///
26617/// NOTE this list also exists in `spg-storage`, which computes the
26618/// signature key from the rendered argument text and has to reach the
26619/// same verdict. The two crates are siblings — neither depends on the
26620/// other — and each already carries its own table of type spellings
26621/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26622/// there), so this follows the structure rather than inventing new
26623/// duplication. Recorded as V49.
26624pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26625 let t = phrase.trim().to_ascii_lowercase();
26626 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26627 matches!(
26628 base,
26629 "double precision"
26630 | "character varying"
26631 | "bit varying"
26632 | "timestamp with time zone"
26633 | "timestamp without time zone"
26634 | "time with time zone"
26635 | "time without time zone"
26636 | "national character"
26637 | "national character varying"
26638 )
26639}
26640
26641fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26642 Some(match ident.to_ascii_lowercase().as_str() {
26643 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26644 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26645 "bigint" => ColumnTypeName::BigInt,
26646 "float" | "double" => ColumnTypeName::Float,
26647 // v7.39 (round 269) — real is 32-bit.
26648 "real" | "float4" => ColumnTypeName::Real,
26649 "text" => ColumnTypeName::Text,
26650 "bool" | "boolean" => ColumnTypeName::Bool,
26651 "date" => ColumnTypeName::Date,
26652 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26653 "timestamptz" => ColumnTypeName::Timestamptz,
26654 "json" => ColumnTypeName::Json,
26655 "jsonb" => ColumnTypeName::Jsonb,
26656 "bytea" | "bytes" => ColumnTypeName::Bytes,
26657 "tsvector" => ColumnTypeName::TsVector,
26658 "tsquery" => ColumnTypeName::TsQuery,
26659 "uuid" => ColumnTypeName::Uuid,
26660 "interval" => ColumnTypeName::Interval,
26661 "time" => ColumnTypeName::Time,
26662 "year" => ColumnTypeName::Year,
26663 "timetz" => ColumnTypeName::TimeTz,
26664 "money" => ColumnTypeName::Money,
26665 _ => return None,
26666 })
26667}
26668
26669/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26670/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26671///
26672/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26673/// / embedded SQL land in v7.12.5+):
26674///
26675/// ```text
26676/// body := [ws] block [ws]
26677/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26678/// stmt := assign | return
26679/// assign := assign_target := expr
26680/// assign_target := ( NEW | OLD ) . ident | ident
26681/// return := RETURN ( NEW | OLD | NULL | expr )
26682/// ```
26683///
26684/// `expr` is parsed by recursing into the regular `Parser` — so a
26685/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26686/// NEW.subject || ' ' || NEW.sender)` body shape works without
26687/// the body parser knowing what `to_tsvector` is.
26688///
26689/// Errors here cause the caller to fall back to
26690/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26691/// successful, but the executor will refuse to invoke the
26692/// function with an "unparseable body" error.
26693/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26694/// from the crate root as `spg_sql::parse_function_body`.
26695pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26696 parse_plpgsql_body(body)
26697}
26698
26699fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26700 // Use the regular lexer on the body text. The trailing
26701 // `END;` may or may not have a semicolon; the lexer treats
26702 // both forms identically.
26703 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26704 message: alloc::format!("plpgsql body lex error: {e}"),
26705 token_pos: 0,
26706 })?;
26707 let mut parser = Parser::new(tokens);
26708 parser.parse_plpgsql_block()
26709}
26710
26711/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26712fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26713 match v {
26714 crate::ast::SetValue::String(s)
26715 | crate::ast::SetValue::Ident(s)
26716 | crate::ast::SetValue::Number(s) => s.clone(),
26717 crate::ast::SetValue::Default => "DEFAULT".into(),
26718 }
26719}
26720
26721/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26722/// contains an aggregate call at ITS OWN query level (recursion stops at
26723/// sublink boundaries — a sublink's aggregates belong to the sublink).
26724/// Backs the "aggregate functions are not allowed in a recursive query's
26725/// recursive term" well-formedness check.
26726fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26727 const AGG_NAMES: &[&str] = &[
26728 "count",
26729 "sum",
26730 "min",
26731 "max",
26732 "avg",
26733 "string_agg",
26734 "array_agg",
26735 "bool_and",
26736 "bool_or",
26737 "every",
26738 "any_value",
26739 "json_agg",
26740 "jsonb_agg",
26741 "json_object_agg",
26742 "jsonb_object_agg",
26743 "bit_and",
26744 "bit_or",
26745 "bit_xor",
26746 "var_pop",
26747 "var_samp",
26748 "variance",
26749 "stddev",
26750 "stddev_pop",
26751 "stddev_samp",
26752 "range_agg",
26753 "range_intersect_agg",
26754 "percentile_cont",
26755 "percentile_disc",
26756 "mode",
26757 "corr",
26758 "covar_pop",
26759 "covar_samp",
26760 ];
26761 match e {
26762 Expr::AggregateOrdered { .. } => true,
26763 Expr::FunctionCall { name, args } => {
26764 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26765 || args.iter().any(expr_has_toplevel_aggregate)
26766 }
26767 Expr::NamedArg { expr, .. }
26768 | Expr::Variadic(expr)
26769 | Expr::Unary { expr, .. }
26770 | Expr::Cast { expr, .. }
26771 | Expr::IsNull { expr, .. }
26772 | Expr::FieldAccess { base: expr, .. }
26773 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26774 Expr::Binary { lhs, rhs, .. } => {
26775 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26776 }
26777 Expr::Like { expr, pattern, .. } => {
26778 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26779 }
26780 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26781 Expr::InList { expr, list, .. } => {
26782 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26783 }
26784 Expr::ArraySubscript { target, index } => {
26785 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26786 }
26787 Expr::ArraySlice { target, lo, hi } => {
26788 expr_has_toplevel_aggregate(target)
26789 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26790 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26791 }
26792 Expr::AnyAll { expr, array, .. } => {
26793 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26794 }
26795 Expr::Case {
26796 operand,
26797 branches,
26798 else_branch,
26799 } => {
26800 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26801 || branches
26802 .iter()
26803 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26804 || else_branch
26805 .as_deref()
26806 .is_some_and(expr_has_toplevel_aggregate)
26807 }
26808 // The outer-level operands of a sublink can aggregate; the sublink's
26809 // own body cannot leak its aggregates up here.
26810 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26811 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26812 row.iter().any(expr_has_toplevel_aggregate)
26813 }
26814 _ => false,
26815 }
26816}
26817
26818/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26819/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26820/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26821/// sublink and is legal in a recursive term, so it is not walked here.
26822fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26823 let mut exprs: Vec<&Expr> = Vec::new();
26824 for it in &s.items {
26825 if let crate::ast::SelectItem::Expr { expr, .. } = it {
26826 exprs.push(expr);
26827 }
26828 }
26829 if let Some(w) = &s.where_ {
26830 exprs.push(w);
26831 }
26832 if let Some(h) = &s.having {
26833 exprs.push(h);
26834 }
26835 if let Some(g) = &s.group_by {
26836 exprs.extend(g.iter());
26837 }
26838 if let Some(from) = &s.from {
26839 for j in &from.joins {
26840 if let Some(on) = &j.on {
26841 exprs.push(on);
26842 }
26843 }
26844 }
26845 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26846}
26847
26848/// Does this expression contain a sublink whose subquery mentions `name`?
26849fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26850 match e {
26851 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26852 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26853 Expr::InSubquery { expr, subquery, .. } => {
26854 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26855 }
26856 Expr::RowInSubquery { row, subquery, .. } => {
26857 row.iter().any(|x| expr_sublink_mentions(x, name))
26858 || select_mentions_table(subquery, name)
26859 }
26860 Expr::RowCmpSubquery { row, subquery, .. } => {
26861 row.iter().any(|x| expr_sublink_mentions(x, name))
26862 || select_mentions_table(subquery, name)
26863 }
26864 Expr::NamedArg { expr, .. }
26865 | Expr::Variadic(expr)
26866 | Expr::Unary { expr, .. }
26867 | Expr::Cast { expr, .. }
26868 | Expr::IsNull { expr, .. }
26869 | Expr::FieldAccess { base: expr, .. }
26870 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26871 Expr::Binary { lhs, rhs, .. } => {
26872 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26873 }
26874 Expr::Like { expr, pattern, .. } => {
26875 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26876 }
26877 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26878 args.iter().any(|x| expr_sublink_mentions(x, name))
26879 }
26880 Expr::InList { expr, list, .. } => {
26881 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26882 }
26883 Expr::ArraySubscript { target, index } => {
26884 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26885 }
26886 Expr::ArraySlice { target, lo, hi } => {
26887 expr_sublink_mentions(target, name)
26888 || lo
26889 .as_deref()
26890 .is_some_and(|x| expr_sublink_mentions(x, name))
26891 || hi
26892 .as_deref()
26893 .is_some_and(|x| expr_sublink_mentions(x, name))
26894 }
26895 Expr::AnyAll { expr, array, .. } => {
26896 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26897 }
26898 Expr::Case {
26899 operand,
26900 branches,
26901 else_branch,
26902 } => {
26903 operand
26904 .as_deref()
26905 .is_some_and(|x| expr_sublink_mentions(x, name))
26906 || branches
26907 .iter()
26908 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26909 || else_branch
26910 .as_deref()
26911 .is_some_and(|x| expr_sublink_mentions(x, name))
26912 }
26913 _ => false,
26914 }
26915}
26916
26917/// Does this SELECT (in full — FROM tables, derived tables, its own
26918/// sublinks, and union arms) mention the named table?
26919fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26920 if let Some(from) = &s.from {
26921 if from.primary.name.eq_ignore_ascii_case(name) {
26922 return true;
26923 }
26924 if let Some(sub) = &from.primary.lateral_subquery
26925 && select_mentions_table(sub, name)
26926 {
26927 return true;
26928 }
26929 for j in &from.joins {
26930 if j.table.name.eq_ignore_ascii_case(name) {
26931 return true;
26932 }
26933 if let Some(sub) = &j.table.lateral_subquery
26934 && select_mentions_table(sub, name)
26935 {
26936 return true;
26937 }
26938 }
26939 }
26940 if select_has_self_ref_in_sublink(s, name) {
26941 return true;
26942 }
26943 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26944}
26945
26946/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26947/// row count, the way PG evaluates one before applying it.
26948///
26949/// `None` = not a constant (a column, a subquery, a function call).
26950/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26951/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26952/// All wordings were read off live PG 18.4.
26953fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26954 use crate::ast::{BinOp, Expr, Literal, UnOp};
26955 match e {
26956 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26957 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26958 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26959 }
26960 // PG coerces a string by its CONTENT, and fails on the value.
26961 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26962 |_| {
26963 Err(alloc::format!(
26964 "invalid input syntax for type bigint: \"{t}\""
26965 ))
26966 },
26967 |n| Ok(i128::from(n)),
26968 )),
26969 Expr::Literal(Literal::Bool(_)) => Some(Err(
26970 "argument of {L} must be type bigint, not type boolean".into(),
26971 )),
26972 Expr::Unary {
26973 op: UnOp::Neg,
26974 expr,
26975 } => match fold_limit_constant(expr)? {
26976 Ok(v) => Some(Ok(-v)),
26977 e @ Err(_) => Some(e),
26978 },
26979 Expr::Binary { lhs, op, rhs } => {
26980 let a = match fold_limit_constant(lhs)? {
26981 Ok(v) => v,
26982 e @ Err(_) => return Some(e),
26983 };
26984 let b = match fold_limit_constant(rhs)? {
26985 Ok(v) => v,
26986 e @ Err(_) => return Some(e),
26987 };
26988 let out = match op {
26989 BinOp::Add => a.checked_add(b),
26990 BinOp::Sub => a.checked_sub(b),
26991 BinOp::Mul => a.checked_mul(b),
26992 BinOp::Div if b != 0 => a.checked_div(b),
26993 BinOp::Div => return Some(Err("division by zero".into())),
26994 BinOp::Mod if b != 0 => a.checked_rem(b),
26995 BinOp::Mod => return Some(Err("division by zero".into())),
26996 _ => return None,
26997 };
26998 // PG evaluates the arithmetic in the operand's own type, so an
26999 // int-by-int product that leaves int range fails there — before
27000 // the row count is ever looked at.
27001 match out {
27002 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27003 Some(Err("integer out of range".into()))
27004 }
27005 Some(v) => Some(Ok(v)),
27006 None => Some(Err("integer out of range".into())),
27007 }
27008 }
27009 _ => None,
27010 }
27011}
27012
27013/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27014/// cast, which is what makes `LIMIT 2.5` keep three rows.
27015fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27016 if scale == 0 {
27017 return unscaled;
27018 }
27019 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27020 return 0;
27021 };
27022 let neg = unscaled < 0;
27023 let mag = unscaled.unsigned_abs() as i128;
27024 let rounded = (mag + div / 2) / div;
27025 if neg { -rounded } else { rounded }
27026}
27027
27028#[cfg(test)]
27029mod tests {
27030 use super::*;
27031 use alloc::string::ToString;
27032
27033 fn parse(s: &str) -> Statement {
27034 parse_statement(s).expect("parse ok")
27035 }
27036
27037 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27038 // `tables`, `partition`, etc. are unreserved keywords per PG's
27039 // `pg_get_keywords()` and MUST be usable as column / table /
27040 // alias names. Pre-T4 every drop-in user whose schema had one
27041 // of these as a column name (sentori events.release, mailrs
27042 // messages.index in some forks) blew the parser up at CREATE
27043 // TABLE time with "expected identifier, got Release". The
27044 // generalisation lives in `unreserved_keyword_text` + the
27045 // `expect_ident_like` and `parse_atom` arms that consult it.
27046 #[test]
27047 fn release_usable_as_column_name_in_create_table() {
27048 let stmt =
27049 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27050 if let Statement::CreateTable(t) = stmt {
27051 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27052 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27053 } else {
27054 panic!("expected CreateTable");
27055 }
27056 }
27057
27058 #[test]
27059 fn release_usable_as_column_ref_in_select_projection() {
27060 // The sentori `0003_partition_events.sql` INSERT-SELECT
27061 // walk references `release` in both column lists; the
27062 // projection-side use exercises `parse_atom`'s relaxed
27063 // identifier set.
27064 parse("SELECT id, release, payload FROM events WHERE id = 1");
27065 }
27066
27067 #[test]
27068 fn release_usable_as_column_ref_in_insert_column_list() {
27069 // INSERT INTO t (id, release, payload) VALUES (…)
27070 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27071 }
27072
27073 #[test]
27074 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27075 // Sentori `0013_audit_tombstone.sql` issues
27076 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27077 // emits Token::Drop (not Ident("drop")); the parser must
27078 // accept both in the ALTER COLUMN sub-dispatch.
27079 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27080 }
27081
27082 #[test]
27083 fn create_index_accepts_parenthesised_expression_key() {
27084 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27085 // expression index. Pre-T4 the parser bailed at the
27086 // inner `(` with "expected column ident or expression,
27087 // got LParen". The Token::LParen arm in CREATE INDEX
27088 // routes through the expression parser instead.
27089 parse(
27090 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27091 ON events ((payload->'bundle'->>'id'))",
27092 );
27093 }
27094
27095 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27096 // surface as parse errors, never stack overflows (embed hosts
27097 // abort on overflow).
27098 /// The nesting budget is a COUNT; what it has to fit inside is a
27099 /// number of BYTES, and only one of those two is stable across
27100 /// compiler versions. Round 847 measured 30,336 bytes per level
27101 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27102 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27103 /// aborted instead of erroring, which is precisely the outcome it
27104 /// exists to rule out.
27105 ///
27106 /// So the budget is metered rather than assumed. The ceiling leaves
27107 /// the depth SPG advertises fitting in a default 2 MiB thread with
27108 /// room to spare, in the debug build, where frames are widest.
27109 #[test]
27110 fn nesting_frame_cost_stays_under_ceiling() {
27111 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27112 // thread keeps a margin for whatever called the parser.
27113 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27114
27115 frame_meter::reset();
27116 let depth = frame_meter::SAMPLE_HI + 8;
27117 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27118 parse(&sql);
27119
27120 let per_level = frame_meter::bytes_per_level();
27121 {
27122 extern crate std;
27123 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27124 }
27125 assert!(
27126 per_level <= CEILING,
27127 "{per_level} bytes per nesting level exceeds {CEILING}; \
27128 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27129 in parse_expr_inner / parse_unary rather than lowering the \
27130 depth or widening the stack.",
27131 per_level * MAX_NEST_DEPTH
27132 );
27133 }
27134
27135 #[test]
27136 fn nesting_budget_errors_cleanly() {
27137 let depth = MAX_NEST_DEPTH + 50;
27138 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27139 let err = parse_statement(&sql).expect_err("must reject");
27140 assert!(err.message.contains("nests deeper"), "{err:?}");
27141 // Within budget still parses.
27142 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27143 parse(&sql);
27144 }
27145
27146 #[test]
27147 fn binary_chain_budget_errors_cleanly() {
27148 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27149 let err = parse_statement(&sql).expect_err("must reject");
27150 assert!(err.message.contains("chained binary"), "{err:?}");
27151 // Within budget still parses (chain depth ≤ budget is safe
27152 // for recursive eval/drop on 2 MiB stacks).
27153 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27154 parse(&sql);
27155 }
27156
27157 #[test]
27158 fn in_list_unaffected_by_chain_budget() {
27159 // Flat InList: 20k elements parse fine and stay flat.
27160 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27161 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27162 let Statement::Select(s) = parse(&sql) else {
27163 panic!("expected select")
27164 };
27165 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27166 panic!("expected flat InList, got {:?}", s.where_)
27167 };
27168 assert_eq!(list.len(), 20_000);
27169 assert!(!negated);
27170 }
27171
27172 fn lit_int(n: i64) -> Expr {
27173 Expr::Literal(Literal::Integer(n))
27174 }
27175
27176 fn col(name: &str) -> Expr {
27177 Expr::Column(ColumnName {
27178 qualifier: None,
27179 name: name.into(),
27180 })
27181 }
27182
27183 #[test]
27184 fn select_single_integer() {
27185 let s = parse("SELECT 1");
27186 let Statement::Select(s) = s else {
27187 panic!("expected SELECT")
27188 };
27189 assert_eq!(s.items.len(), 1);
27190 assert!(s.from.is_none());
27191 assert!(s.where_.is_none());
27192 }
27193
27194 #[test]
27195 fn select_multiple_literal_kinds() {
27196 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27197 let Statement::Select(s) = s else {
27198 panic!("expected SELECT")
27199 };
27200 assert_eq!(s.items.len(), 5);
27201 }
27202
27203 #[test]
27204 fn select_wildcard_from_table() {
27205 let s = parse("SELECT * FROM users");
27206 let Statement::Select(s) = s else {
27207 panic!("expected SELECT")
27208 };
27209 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27210 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27211 }
27212
27213 #[test]
27214 fn select_with_table_alias() {
27215 let s = parse("SELECT * FROM users AS u");
27216 let Statement::Select(s) = s else {
27217 panic!("expected SELECT")
27218 };
27219 let t = &s.from.as_ref().unwrap().primary;
27220 assert_eq!(t.name, "users");
27221 assert_eq!(t.alias.as_deref(), Some("u"));
27222 }
27223
27224 #[test]
27225 fn select_with_where_eq() {
27226 let s = parse("SELECT a FROM t WHERE a = 1");
27227 let Statement::Select(s) = s else {
27228 panic!("expected SELECT")
27229 };
27230 let w = s.where_.unwrap();
27231 assert_eq!(
27232 w,
27233 Expr::Binary {
27234 lhs: Box::new(col("a")),
27235 op: BinOp::Eq,
27236 rhs: Box::new(lit_int(1)),
27237 }
27238 );
27239 }
27240
27241 #[test]
27242 fn arithmetic_precedence() {
27243 let s = parse("SELECT 1 + 2 * 3");
27244 let Statement::Select(s) = s else {
27245 panic!("expected SELECT")
27246 };
27247 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27248 panic!("wildcard?")
27249 };
27250 assert_eq!(
27251 expr,
27252 &Expr::Binary {
27253 lhs: Box::new(lit_int(1)),
27254 op: BinOp::Add,
27255 rhs: Box::new(Expr::Binary {
27256 lhs: Box::new(lit_int(2)),
27257 op: BinOp::Mul,
27258 rhs: Box::new(lit_int(3)),
27259 }),
27260 }
27261 );
27262 }
27263
27264 #[test]
27265 fn parentheses_override_precedence() {
27266 let s = parse("SELECT (1 + 2) * 3");
27267 let Statement::Select(s) = s else {
27268 panic!("expected SELECT")
27269 };
27270 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27271 panic!()
27272 };
27273 assert_eq!(
27274 expr,
27275 &Expr::Binary {
27276 lhs: Box::new(Expr::Binary {
27277 lhs: Box::new(lit_int(1)),
27278 op: BinOp::Add,
27279 rhs: Box::new(lit_int(2)),
27280 }),
27281 op: BinOp::Mul,
27282 rhs: Box::new(lit_int(3)),
27283 }
27284 );
27285 }
27286
27287 #[test]
27288 fn not_binds_below_comparison() {
27289 // `NOT a = 1` should parse as `NOT (a = 1)`.
27290 let s = parse("SELECT NOT a = 1 FROM t");
27291 let Statement::Select(s) = s else {
27292 panic!("expected SELECT")
27293 };
27294 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27295 panic!()
27296 };
27297 assert_eq!(
27298 expr,
27299 &Expr::Unary {
27300 op: UnOp::Not,
27301 expr: Box::new(Expr::Binary {
27302 lhs: Box::new(col("a")),
27303 op: BinOp::Eq,
27304 rhs: Box::new(lit_int(1)),
27305 }),
27306 }
27307 );
27308 }
27309
27310 #[test]
27311 fn unary_minus_binds_above_multiplication() {
27312 // `-a * 2` should be `(-a) * 2`.
27313 let s = parse("SELECT -a * 2 FROM t");
27314 let Statement::Select(s) = s else {
27315 panic!("expected SELECT")
27316 };
27317 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27318 panic!()
27319 };
27320 assert_eq!(
27321 expr,
27322 &Expr::Binary {
27323 lhs: Box::new(Expr::Unary {
27324 op: UnOp::Neg,
27325 expr: Box::new(col("a")),
27326 }),
27327 op: BinOp::Mul,
27328 rhs: Box::new(lit_int(2)),
27329 }
27330 );
27331 }
27332
27333 #[test]
27334 fn qualified_column() {
27335 let s = parse("SELECT t.col FROM t");
27336 let Statement::Select(s) = s else {
27337 panic!("expected SELECT")
27338 };
27339 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27340 panic!()
27341 };
27342 assert_eq!(
27343 expr,
27344 &Expr::Column(ColumnName {
27345 qualifier: Some("t".into()),
27346 name: "col".into()
27347 })
27348 );
27349 }
27350
27351 #[test]
27352 fn select_item_alias_with_as() {
27353 let s = parse("SELECT a AS y FROM t");
27354 let Statement::Select(s) = s else {
27355 panic!("expected SELECT")
27356 };
27357 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27358 panic!()
27359 };
27360 assert_eq!(alias.as_deref(), Some("y"));
27361 }
27362
27363 #[test]
27364 fn trailing_semicolon_accepted() {
27365 let s = parse("SELECT 1;");
27366 let Statement::Select(s) = s else {
27367 panic!("expected SELECT")
27368 };
27369 assert_eq!(s.items.len(), 1);
27370 }
27371
27372 #[test]
27373 fn boolean_chain_with_and_or_not() {
27374 // (NOT a) OR (b AND (NOT c))
27375 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27376 let Statement::Select(s) = s else {
27377 panic!("expected SELECT")
27378 };
27379 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27380 panic!()
27381 };
27382 let expected = Expr::Binary {
27383 lhs: Box::new(Expr::Unary {
27384 op: UnOp::Not,
27385 expr: Box::new(col("a")),
27386 }),
27387 op: BinOp::Or,
27388 rhs: Box::new(Expr::Binary {
27389 lhs: Box::new(col("b")),
27390 op: BinOp::And,
27391 rhs: Box::new(Expr::Unary {
27392 op: UnOp::Not,
27393 expr: Box::new(col("c")),
27394 }),
27395 }),
27396 };
27397 assert_eq!(expr, &expected);
27398 }
27399
27400 #[test]
27401 fn empty_input_errors() {
27402 // v7.14.0 — pg_dump preambles emit several comment-only
27403 // / blank-line statements that collapse to Statement::
27404 // Empty rather than a parse error. The old "SELECT in
27405 // message" assertion is stale; verify the new contract:
27406 // empty / whitespace / comment-only input parses to
27407 // Statement::Empty.
27408 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27409 assert!(matches!(
27410 parse_statement(" \n\t ").unwrap(),
27411 Statement::Empty
27412 ));
27413 // Sanity: malformed-but-non-empty still errors.
27414 assert!(parse_statement("SELECT FROM WHERE").is_err());
27415 }
27416
27417 #[test]
27418 fn unmatched_paren_errors() {
27419 assert!(parse_statement("SELECT (1 + 2").is_err());
27420 }
27421
27422 #[test]
27423 fn display_round_trip_simple_select() {
27424 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27425 let text = original.to_string();
27426 let again = parse_statement(&text).expect("re-parse");
27427 assert_eq!(original, again);
27428 }
27429
27430 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27431
27432 #[test]
27433 fn create_table_single_column() {
27434 let s = parse("CREATE TABLE foo (a INT)");
27435 let Statement::CreateTable(c) = s else {
27436 panic!("expected CreateTable")
27437 };
27438 assert_eq!(c.name, "foo");
27439 assert_eq!(c.columns.len(), 1);
27440 assert_eq!(c.columns[0].name, "a");
27441 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27442 assert!(c.columns[0].nullable);
27443 }
27444
27445 #[test]
27446 fn create_table_multi_column_with_not_null_mix() {
27447 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27448 let Statement::CreateTable(c) = s else {
27449 panic!()
27450 };
27451 assert_eq!(c.columns.len(), 4);
27452 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27453 assert!(!c.columns[0].nullable);
27454 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27455 assert!(c.columns[1].nullable);
27456 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27457 assert!(!c.columns[2].nullable);
27458 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27459 }
27460
27461 #[test]
27462 fn create_table_bigint_supported() {
27463 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27464 let Statement::CreateTable(c) = s else {
27465 panic!()
27466 };
27467 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27468 }
27469
27470 #[test]
27471 fn create_table_vector_default_is_f32() {
27472 let s = parse("CREATE TABLE t (v VECTOR(128))");
27473 let Statement::CreateTable(c) = s else {
27474 panic!()
27475 };
27476 assert_eq!(
27477 c.columns[0].ty,
27478 ColumnTypeName::Vector {
27479 dim: 128,
27480 encoding: VecEncoding::F32,
27481 },
27482 );
27483 }
27484
27485 #[test]
27486 fn create_table_vector_using_sq8() {
27487 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27488 // Case-insensitive on both `USING` and the encoding name.
27489 for sql in [
27490 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27491 "CREATE TABLE t (v VECTOR(128) using sq8)",
27492 ] {
27493 let s = parse(sql);
27494 let Statement::CreateTable(c) = s else {
27495 panic!()
27496 };
27497 assert_eq!(
27498 c.columns[0].ty,
27499 ColumnTypeName::Vector {
27500 dim: 128,
27501 encoding: VecEncoding::Sq8,
27502 },
27503 "{sql}",
27504 );
27505 }
27506 }
27507
27508 #[test]
27509 fn create_table_vector_using_unknown_errors() {
27510 // v7.16.1 — the inline `USING <encoding>` shape on
27511 // CREATE TABLE column defs was withdrawn before
27512 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27513 // (col vector_<metric>_ops)`; the parser now rejects
27514 // USING at column-list position with a clearer
27515 // "expected ',' or ')'" message. Test asserts the
27516 // current rejection, not the old "unknown vector
27517 // encoding" string.
27518 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27519 assert!(
27520 err.message.contains("USING")
27521 || err.message.contains("using")
27522 || err.message.contains("')'")
27523 || err.message.contains("','"),
27524 "expected USING/column-list rejection, got: {}",
27525 err.message
27526 );
27527 }
27528
27529 #[test]
27530 fn vector_using_sq8_display_roundtrips() {
27531 // The Display impl must produce text that re-parses to the
27532 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27533 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27534 let Statement::CreateTable(c) = s else {
27535 panic!()
27536 };
27537 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27538 }
27539
27540 #[test]
27541 fn parser_recognises_placeholders() {
27542 use crate::ast::{Expr, SelectItem, Statement};
27543 // $N in expression position parses as Expr::Placeholder(N).
27544 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27545 let Statement::Select(sel) = s else { panic!() };
27546 assert!(matches!(
27547 sel.items[0],
27548 SelectItem::Expr {
27549 expr: Expr::Placeholder(1),
27550 alias: None
27551 }
27552 ));
27553 // $2 + 1
27554 let SelectItem::Expr {
27555 expr: Expr::Binary { lhs, rhs, .. },
27556 ..
27557 } = &sel.items[1]
27558 else {
27559 panic!()
27560 };
27561 assert!(matches!(**lhs, Expr::Placeholder(2)));
27562 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27563 // WHERE x = $3
27564 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27565 panic!()
27566 };
27567 assert!(matches!(**rhs, Expr::Placeholder(3)));
27568 }
27569
27570 #[test]
27571 fn parser_rejects_dollar_zero() {
27572 // $0 is not valid in PG; the lexer rejects it.
27573 assert!(parse_statement("SELECT $0").is_err());
27574 }
27575
27576 #[test]
27577 fn placeholder_display_roundtrips() {
27578 // The Display impl must produce text that re-lexes to the
27579 // same Placeholder token.
27580 let s = parse("SELECT $42 FROM t");
27581 let printed = s.to_string();
27582 assert!(printed.contains("$42"));
27583 let again = parse(&printed);
27584 assert_eq!(s, again);
27585 }
27586
27587 #[test]
27588 fn alter_index_rebuild_bare() {
27589 use crate::ast::{AlterIndexTarget, Statement};
27590 let s = parse("ALTER INDEX my_idx REBUILD");
27591 let Statement::AlterIndex(a) = s else {
27592 panic!("expected AlterIndex, got {s:?}")
27593 };
27594 assert_eq!(a.name, "my_idx");
27595 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27596 }
27597
27598 #[test]
27599 fn alter_index_rebuild_with_encoding() {
27600 use crate::ast::{AlterIndexTarget, Statement};
27601 for (sql, want) in [
27602 (
27603 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27604 VecEncoding::F32,
27605 ),
27606 (
27607 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27608 VecEncoding::Sq8,
27609 ),
27610 (
27611 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27612 VecEncoding::F16,
27613 ),
27614 ] {
27615 let s = parse(sql);
27616 let Statement::AlterIndex(a) = s else {
27617 panic!("{sql}: expected AlterIndex")
27618 };
27619 assert_eq!(a.name, "my_idx");
27620 assert_eq!(
27621 a.target,
27622 AlterIndexTarget::Rebuild {
27623 encoding: Some(want)
27624 },
27625 "{sql}"
27626 );
27627 }
27628 }
27629
27630 #[test]
27631 fn alter_index_rebuild_unknown_encoding_errors() {
27632 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27633 assert!(
27634 err.message.contains("unknown vector encoding"),
27635 "got: {}",
27636 err.message
27637 );
27638 }
27639
27640 #[test]
27641 fn alter_index_rebuild_display_roundtrips() {
27642 for (input, want) in [
27643 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27644 (
27645 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27646 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27647 ),
27648 (
27649 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27650 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27651 ),
27652 ] {
27653 let s = parse(input);
27654 assert_eq!(s.to_string(), want);
27655 }
27656 }
27657
27658 #[test]
27659 fn create_table_unknown_type_defers_to_engine() {
27660 // v4.9 picked XML as a parse-time "unsupported column
27661 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27662 // an unknown type ident parses as Text + `user_type_ref`
27663 // so CREATE TABLE can resolve user-defined enum / domain
27664 // types — rejection of truly-unknown types moved to the
27665 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27666 // to a first-class built-in, so this probe switched to a
27667 // synthetic name nothing in the lexer will ever recognise.
27668 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27669 let Statement::CreateTable(t) = stmt else {
27670 panic!("expected CreateTable");
27671 };
27672 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27673 }
27674
27675 #[test]
27676 fn create_table_missing_table_keyword_errors() {
27677 assert!(parse_statement("CREATE x (a INT)").is_err());
27678 }
27679
27680 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27681 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27682
27683 #[test]
27684 fn parse_create_table_partition_by_range() {
27685 use crate::ast::{PartitionBySpec, PartitionKindAst};
27686 let stmt = parse_statement(
27687 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27688 payload JSONB) PARTITION BY RANGE (ts)",
27689 )
27690 .unwrap();
27691 let Statement::CreateTable(t) = stmt else {
27692 panic!("expected CreateTable");
27693 };
27694 assert!(t.partition_of.is_none(), "parent has no partition_of");
27695 assert_eq!(t.columns.len(), 3);
27696 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27697 assert_eq!(
27698 by,
27699 &PartitionBySpec {
27700 kind: PartitionKindAst::Range,
27701 key_columns: alloc::vec!["ts".to_string()],
27702 }
27703 );
27704 // Display round-trip preserves the suffix. `quote_ident`
27705 // only adds double quotes when the ident needs escaping, so
27706 // a plain `ts` survives bare here.
27707 assert!(
27708 t.to_string().contains("PARTITION BY RANGE (ts)"),
27709 "Display lost PARTITION BY suffix: {t}"
27710 );
27711 }
27712
27713 #[test]
27714 fn parse_create_table_partition_of_range() {
27715 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27716 let stmt = parse_statement(
27717 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27718 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27719 )
27720 .unwrap();
27721 let Statement::CreateTable(t) = stmt else {
27722 panic!("expected CreateTable");
27723 };
27724 assert!(t.columns.is_empty(), "child inherits columns from parent");
27725 assert!(t.partition_by.is_none());
27726 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27727 assert_eq!(of.parent_name, "events_partitioned");
27728 let PartitionOfSpec { bounds, .. } = of.clone();
27729 match bounds {
27730 PartitionOfBoundsAst::Range { lower, upper } => {
27731 assert!(lower.to_string().contains("2026-06-01"));
27732 assert!(upper.to_string().contains("2026-07-01"));
27733 }
27734 other => panic!("expected Range, got {other:?}"),
27735 }
27736 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27737 // skips quotes when not required, so the parent name appears
27738 // bare here.
27739 let s = t.to_string();
27740 assert!(
27741 s.contains("PARTITION OF events_partitioned"),
27742 "Display lost PARTITION OF: {s}"
27743 );
27744 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27745 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27746 }
27747
27748 #[test]
27749 fn parse_create_table_partition_of_default() {
27750 use crate::ast::PartitionOfBoundsAst;
27751 let stmt =
27752 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27753 .unwrap();
27754 let Statement::CreateTable(t) = stmt else {
27755 panic!("expected CreateTable");
27756 };
27757 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27758 assert_eq!(of.parent_name, "events_partitioned");
27759 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27760 assert!(
27761 t.to_string()
27762 .contains("PARTITION OF events_partitioned DEFAULT"),
27763 "Display lost DEFAULT: {t}"
27764 );
27765 }
27766
27767 #[test]
27768 fn parse_create_table_partition_by_list() {
27769 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27770 // child with `FOR VALUES IN (lit, lit, …)`.
27771 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27772 let parent =
27773 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27774 .unwrap();
27775 let Statement::CreateTable(t) = parent else {
27776 panic!("expected CreateTable");
27777 };
27778 let Some(PartitionBySpec {
27779 kind,
27780 ref key_columns,
27781 }) = t.partition_by
27782 else {
27783 panic!("expected PARTITION BY");
27784 };
27785 assert_eq!(kind, PartitionKindAst::List);
27786 assert_eq!(*key_columns, vec!["region".to_string()]);
27787 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27788
27789 let child = parse_statement(
27790 "CREATE TABLE events_apac PARTITION OF events_listed \
27791 FOR VALUES IN ('jp', 'kr', 'tw')",
27792 )
27793 .unwrap();
27794 let Statement::CreateTable(c) = child else {
27795 panic!("expected CreateTable");
27796 };
27797 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27798 let PartitionOfBoundsAst::List { values } = &of.bounds else {
27799 panic!("expected List bounds, got {:?}", of.bounds);
27800 };
27801 assert_eq!(values.len(), 3);
27802 let disp = c.to_string();
27803 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27804 }
27805
27806 #[test]
27807 fn parse_create_table_partition_by_hash() {
27808 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27809 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27810 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27811 let parent =
27812 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27813 let Statement::CreateTable(t) = parent else {
27814 panic!("expected CreateTable");
27815 };
27816 let Some(PartitionBySpec {
27817 kind,
27818 ref key_columns,
27819 }) = t.partition_by
27820 else {
27821 panic!("expected PARTITION BY");
27822 };
27823 assert_eq!(kind, PartitionKindAst::Hash);
27824 assert_eq!(*key_columns, vec!["id".to_string()]);
27825 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27826
27827 let child = parse_statement(
27828 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27829 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27830 )
27831 .unwrap();
27832 let Statement::CreateTable(c) = child else {
27833 panic!("expected CreateTable");
27834 };
27835 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27836 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27837 panic!("expected Hash bounds");
27838 };
27839 assert_eq!(modulus, 4);
27840 assert_eq!(remainder, 0);
27841 let disp = c.to_string();
27842 assert!(
27843 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27844 "Display lost HASH bounds: {disp}"
27845 );
27846
27847 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27848 let bad = parse_statement(
27849 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27850 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27851 );
27852 let msg = format!("{}", bad.unwrap_err());
27853 assert!(
27854 msg.contains("REMAINDER") && msg.contains("MODULUS"),
27855 "expected REMAINDER/MODULUS validation error: {msg}"
27856 );
27857 }
27858
27859 #[test]
27860 fn parse_create_table_partition_of_rejects_columns() {
27861 // v7.37.6-B contract: PARTITION OF children inherit columns
27862 // from the parent; an explicit list MUST surface as a parse
27863 // error rather than getting silently ignored.
27864 let err = parse_statement(
27865 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27866 FOR VALUES FROM ('a') TO ('b')",
27867 );
27868 assert!(err.is_err(), "expected parse error for explicit columns");
27869 let msg = format!("{}", err.unwrap_err());
27870 assert!(
27871 msg.contains("PARTITION OF") && msg.contains("column"),
27872 "error should mention PARTITION OF + columns: {msg}"
27873 );
27874 }
27875
27876 #[test]
27877 fn insert_single_value() {
27878 let s = parse("INSERT INTO foo VALUES (42)");
27879 let Statement::Insert(i) = s else {
27880 panic!("expected Insert")
27881 };
27882 assert_eq!(i.table, "foo");
27883 assert_eq!(i.rows.len(), 1);
27884 assert_eq!(i.rows[0].len(), 1);
27885 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27886 }
27887
27888 #[test]
27889 fn insert_multi_value_with_mixed_literals() {
27890 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27891 let Statement::Insert(i) = s else { panic!() };
27892 assert_eq!(i.rows.len(), 1);
27893 assert_eq!(i.rows[0].len(), 5);
27894 }
27895
27896 #[test]
27897 fn insert_missing_into_errors() {
27898 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27899 }
27900
27901 #[test]
27902 fn create_table_round_trip() {
27903 let original =
27904 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27905 let text = original.to_string();
27906 let again = parse_statement(&text).expect("re-parse");
27907 assert_eq!(original, again);
27908 }
27909
27910 #[test]
27911 fn insert_round_trip_with_negation_and_string() {
27912 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27913 let text = original.to_string();
27914 let again = parse_statement(&text).expect("re-parse");
27915 assert_eq!(original, again);
27916 }
27917
27918 #[test]
27919 fn unknown_keyword_at_statement_start_errors() {
27920 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27921 // the top-level dispatch still has no branch to take.
27922 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27923 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27924 }
27925
27926 // --- v0.8 CREATE INDEX --------------------------------------------------
27927
27928 #[test]
27929 fn create_index_basic() {
27930 let s = parse("CREATE INDEX idx_id ON users (id)");
27931 let Statement::CreateIndex(c) = s else {
27932 panic!("expected CreateIndex")
27933 };
27934 assert_eq!(c.name, "idx_id");
27935 assert_eq!(c.table, "users");
27936 assert_eq!(c.column, "id");
27937 }
27938
27939 #[test]
27940 fn create_index_missing_on_errors() {
27941 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27942 }
27943
27944 #[test]
27945 fn create_index_missing_paren_errors() {
27946 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27947 }
27948
27949 #[test]
27950 fn create_index_round_trip() {
27951 let original = parse("CREATE INDEX by_name ON users (name)");
27952 let again = parse_statement(&original.to_string()).unwrap();
27953 assert_eq!(original, again);
27954 }
27955
27956 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27957
27958 #[test]
27959 fn create_unique_index_basic() {
27960 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27961 let Statement::CreateIndex(c) = s else {
27962 panic!("expected CreateIndex");
27963 };
27964 assert!(c.is_unique);
27965 assert_eq!(c.column, "a");
27966 assert!(c.partial_predicate.is_none());
27967 }
27968
27969 #[test]
27970 fn create_unique_index_partial() {
27971 // mailrs's email_templates "one default per user" shape.
27972 let s = parse(
27973 "CREATE UNIQUE INDEX idx_email_templates_user_default \
27974 ON email_templates (user_address) WHERE is_default = true",
27975 );
27976 let Statement::CreateIndex(c) = s else {
27977 panic!("expected CreateIndex");
27978 };
27979 assert!(c.is_unique);
27980 assert_eq!(c.table, "email_templates");
27981 assert_eq!(c.column, "user_address");
27982 assert!(c.partial_predicate.is_some());
27983 }
27984
27985 #[test]
27986 fn create_unique_index_composite_with_predicate() {
27987 // mailrs's calendar_events instance: composite columns.
27988 let s = parse(
27989 "CREATE UNIQUE INDEX uq_calendar_events_instance \
27990 ON calendar_events (calendar_id, uid, recurrence_id) \
27991 WHERE recurrence_id IS NOT NULL",
27992 );
27993 let Statement::CreateIndex(c) = s else {
27994 panic!("expected CreateIndex");
27995 };
27996 assert!(c.is_unique);
27997 assert_eq!(c.column, "calendar_id");
27998 assert_eq!(
27999 c.extra_columns,
28000 vec!["uid".to_string(), "recurrence_id".to_string()]
28001 );
28002 assert!(c.partial_predicate.is_some());
28003 }
28004
28005 #[test]
28006 fn create_unique_index_using_btree_ok() {
28007 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28008 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28009 }
28010
28011 #[test]
28012 fn create_unique_index_using_hnsw_rejected() {
28013 let err =
28014 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28015 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28016 }
28017
28018 #[test]
28019 fn create_unique_index_round_trip() {
28020 let original = parse(
28021 "CREATE UNIQUE INDEX uq_calendar_events_master \
28022 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28023 );
28024 let again = parse_statement(&original.to_string()).unwrap();
28025 assert_eq!(original, again);
28026 }
28027
28028 #[test]
28029 fn create_unique_without_index_errors() {
28030 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28031 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28032 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28033 }
28034
28035 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28036
28037 #[test]
28038 fn create_table_bytea_column() {
28039 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28040 let Statement::CreateTable(c) = s else {
28041 panic!("expected CreateTable");
28042 };
28043 assert_eq!(c.columns.len(), 2);
28044 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28045 assert!(!c.columns[1].nullable);
28046 }
28047
28048 #[test]
28049 fn create_table_bytes_alias_column() {
28050 let s = parse("CREATE TABLE t (blob BYTES)");
28051 let Statement::CreateTable(c) = s else {
28052 panic!("expected CreateTable");
28053 };
28054 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28055 }
28056
28057 #[test]
28058 fn bytea_round_trip_display() {
28059 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28060 let again = parse_statement(&original.to_string()).unwrap();
28061 assert_eq!(original, again);
28062 }
28063
28064 // --- v0.9 transactions -------------------------------------------------
28065
28066 #[test]
28067 fn begin_commit_rollback_parse_as_unit_variants() {
28068 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28069 assert_eq!(parse("COMMIT"), Statement::Commit);
28070 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28071 // Trailing semicolons accepted too.
28072 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28073 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28074 // statement (with or without the WORK/TRANSACTION noise word).
28075 assert_eq!(
28076 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28077 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28078 );
28079 assert_eq!(
28080 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28081 Statement::Begin(Some(IsolationLevel::Serializable))
28082 );
28083 // A non-isolation mode keeps the session default (None).
28084 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28085 }
28086
28087 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28088
28089 #[test]
28090 fn inner_product_binop_parses() {
28091 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28092 let Statement::Select(s) = s else { panic!() };
28093 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28094 panic!()
28095 };
28096 assert!(matches!(
28097 expr,
28098 Expr::Binary {
28099 op: BinOp::InnerProduct,
28100 ..
28101 }
28102 ));
28103 }
28104
28105 #[test]
28106 fn cosine_distance_binop_parses() {
28107 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28108 let Statement::Select(s) = s else { panic!() };
28109 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28110 panic!()
28111 };
28112 assert!(matches!(
28113 expr,
28114 Expr::Binary {
28115 op: BinOp::CosineDistance,
28116 ..
28117 }
28118 ));
28119 }
28120
28121 #[test]
28122 fn vector_cast_postfix_wraps_string_literal() {
28123 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28124 let Statement::Select(s) = s else { panic!() };
28125 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28126 panic!()
28127 };
28128 assert!(matches!(
28129 expr,
28130 Expr::Cast {
28131 target: CastTarget::Vector,
28132 ..
28133 }
28134 ));
28135 }
28136
28137 #[test]
28138 fn unsupported_cast_target_errors() {
28139 // v7.37.5 ship triage promoted the parser to accept every
28140 // ident as a `CastTarget::Named(canonical)`; the engine
28141 // surfaces the "unsupported cast target" error at eval
28142 // time when `type_name_to_data_type` can't resolve it.
28143 // Parser-side error now requires a NON-ident after `::`
28144 // (e.g. a punctuation token).
28145 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28146 assert_eq!(err.message, "syntax error at or near \",\"");
28147 }
28148
28149 #[test]
28150 fn tx_statements_round_trip() {
28151 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28152 let original = parse(q);
28153 let again = parse_statement(&original.to_string()).unwrap();
28154 assert_eq!(original, again);
28155 }
28156 }
28157
28158 #[test]
28159 fn interval_text_parsing_units() {
28160 // v7.37.5 β — three-field shape `(months, days, micros)` so
28161 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28162 // Single unit.
28163 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28164 assert_eq!(
28165 parse_interval_text("24 hours"),
28166 Some((0, 0, 86_400_000_000))
28167 );
28168 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28169 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28170 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28171 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28172 // Compound spans accumulate per-dimension.
28173 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28174 assert_eq!(
28175 parse_interval_text("1 day 2 hours"),
28176 Some((0, 1, 7_200_000_000))
28177 );
28178 // Negative numbers carry through per-dimension.
28179 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28180 // Bad shapes return None.
28181 assert_eq!(parse_interval_text(""), None);
28182 assert_eq!(parse_interval_text("garbage"), None);
28183 assert_eq!(parse_interval_text("1 fortnight"), None);
28184 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28185 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28186 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28187 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28188 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28189 }
28190
28191 #[test]
28192 fn interval_literal_roundtrips_via_display() {
28193 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28194 let s = parsed.to_string();
28195 // Display preserves the original text verbatim.
28196 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28197 // And re-parsing yields a structurally equal statement.
28198 let again = parse_statement(&s).unwrap();
28199 assert_eq!(parsed, again);
28200 }
28201
28202 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28203
28204 #[test]
28205 fn parser_recognises_create_publication_bare() {
28206 let s = parse("CREATE PUBLICATION pub_a");
28207 let Statement::CreatePublication(p) = s else {
28208 panic!("expected CreatePublication, got {s:?}")
28209 };
28210 assert_eq!(p.name, "pub_a");
28211 assert_eq!(p.scope, PublicationScope::AllTables);
28212 }
28213
28214 #[test]
28215 fn parser_recognises_create_publication_for_all_tables() {
28216 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28217 let Statement::CreatePublication(p) = s else {
28218 panic!("expected CreatePublication, got {s:?}")
28219 };
28220 assert_eq!(p.name, "pub_a");
28221 assert_eq!(p.scope, PublicationScope::AllTables);
28222 }
28223
28224 #[test]
28225 fn parser_recognises_drop_publication() {
28226 let s = parse("DROP PUBLICATION pub_a");
28227 let Statement::DropPublication { name, .. } = s else {
28228 panic!("expected DropPublication, got {s:?}")
28229 };
28230 assert_eq!(name, "pub_a");
28231 }
28232
28233 #[test]
28234 fn parser_recognises_for_table_list() {
28235 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28236 let Statement::CreatePublication(p) = s else {
28237 panic!("expected CreatePublication, got {s:?}")
28238 };
28239 assert_eq!(p.name, "pub_a");
28240 let PublicationScope::ForTables(ts) = p.scope else {
28241 panic!("expected ForTables scope")
28242 };
28243 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28244 }
28245
28246 #[test]
28247 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28248 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28249 // is rejected (`invalid publication object list`; the old
28250 // test pinned an unverifiable "PG 19 accepts both" claim);
28251 // TABLES pairs with IN SCHEMA.
28252 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28253 .expect_err("bare FOR TABLES must reject");
28254 assert!(
28255 alloc::format!("{err}").contains("invalid publication object list"),
28256 "got: {err}"
28257 );
28258 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28259 let Statement::CreatePublication(p) = s else {
28260 panic!("expected CreatePublication, got {s:?}")
28261 };
28262 let PublicationScope::TablesInSchema(schema) = p.scope else {
28263 panic!("expected TablesInSchema")
28264 };
28265 assert_eq!(schema, "public");
28266 }
28267
28268 #[test]
28269 fn parser_recognises_for_all_tables_except_list() {
28270 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28271 let Statement::CreatePublication(p) = s else {
28272 panic!()
28273 };
28274 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28275 panic!("expected AllTablesExcept")
28276 };
28277 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28278 }
28279
28280 #[test]
28281 fn parser_rejects_for_table_with_empty_list() {
28282 // `FOR TABLE` with nothing after is a parse error.
28283 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28284 .expect_err("must error on empty list");
28285 // No specific message asserted — the call falls through to
28286 // expect_ident_like which yields "expected identifier, got …".
28287 assert!(!err.message.is_empty());
28288 }
28289
28290 #[test]
28291 fn parser_recognises_show_publications() {
28292 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28293 // bare ident in this position, NOT a reserved keyword.
28294 let s = parse("SHOW PUBLICATIONS");
28295 assert!(matches!(s, Statement::ShowPublications));
28296 }
28297
28298 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28299
28300 #[test]
28301 fn parser_recognises_create_subscription_single_publication() {
28302 let s = parse(
28303 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28304 );
28305 let Statement::CreateSubscription(c) = s else {
28306 panic!("expected CreateSubscription, got {s:?}")
28307 };
28308 assert_eq!(c.name, "sub_a");
28309 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28310 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28311 }
28312
28313 #[test]
28314 fn parser_recognises_create_subscription_multi_publication() {
28315 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28316 let Statement::CreateSubscription(c) = s else {
28317 panic!()
28318 };
28319 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28320 }
28321
28322 #[test]
28323 fn parser_rejects_create_subscription_missing_connection() {
28324 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28325 .expect_err("must error on missing CONNECTION");
28326 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28327 }
28328
28329 #[test]
28330 fn parser_rejects_create_subscription_missing_publication() {
28331 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28332 .expect_err("must error on missing PUBLICATION");
28333 assert_eq!(err.message, "syntax error at end of input");
28334 }
28335
28336 #[test]
28337 fn parser_recognises_drop_subscription() {
28338 let s = parse("DROP SUBSCRIPTION sub_a");
28339 let Statement::DropSubscription { name, .. } = s else {
28340 panic!("expected DropSubscription, got {s:?}")
28341 };
28342 assert_eq!(name, "sub_a");
28343 }
28344
28345 #[test]
28346 fn parser_recognises_show_subscriptions() {
28347 let s = parse("SHOW SUBSCRIPTIONS");
28348 assert!(matches!(s, Statement::ShowSubscriptions));
28349 }
28350
28351 #[test]
28352 fn parser_recognises_wait_for_wal_position_no_timeout() {
28353 let s = parse("WAIT FOR WAL POSITION 12345");
28354 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28355 panic!("expected WaitForWalPosition, got {s:?}")
28356 };
28357 assert_eq!(pos, 12345);
28358 assert!(timeout_ms.is_none());
28359 }
28360
28361 #[test]
28362 fn parser_recognises_wait_for_wal_position_with_timeout() {
28363 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28364 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28365 panic!()
28366 };
28367 assert_eq!(pos, 67890);
28368 assert_eq!(timeout_ms, Some(5000));
28369 }
28370
28371 #[test]
28372 fn parser_rejects_wait_with_negative_position() {
28373 // The lexer treats `-` as a token; `expect_u64_literal`
28374 // only sees the Integer that follows, so the negative
28375 // arrives as a unary-minus expression at higher levels.
28376 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28377 // parse error one way or another.
28378 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28379 assert!(!err.message.is_empty());
28380 }
28381
28382 #[test]
28383 fn parser_recognises_bare_analyze() {
28384 let s = parse("ANALYZE");
28385 assert!(matches!(s, Statement::Analyze(None)));
28386 }
28387
28388 #[test]
28389 fn parser_recognises_analyze_with_table() {
28390 let s = parse("ANALYZE users");
28391 let Statement::Analyze(Some(name)) = s else {
28392 panic!("expected Analyze, got {s:?}")
28393 };
28394 assert_eq!(name, "users");
28395 }
28396
28397 #[test]
28398 fn parser_recognises_analyze_with_quoted_table() {
28399 let s = parse("ANALYZE \"Mixed Case\"");
28400 let Statement::Analyze(Some(name)) = s else {
28401 panic!()
28402 };
28403 assert_eq!(name, "Mixed Case");
28404 }
28405
28406 #[test]
28407 fn parser_rejects_analyze_with_garbage_token() {
28408 let err = parse_statement("ANALYZE 42").expect_err("must error");
28409 assert!(!err.message.is_empty());
28410 }
28411
28412 #[test]
28413 fn analyze_display_roundtrips() {
28414 for sql in ["ANALYZE", "ANALYZE users"] {
28415 let s = parse(sql);
28416 let printed = s.to_string();
28417 let again = parse_statement(&printed)
28418 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28419 assert_eq!(s, again);
28420 }
28421 }
28422
28423 #[test]
28424 fn wait_for_display_roundtrips() {
28425 for sql in [
28426 "WAIT FOR WAL POSITION 12345",
28427 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28428 ] {
28429 let s = parse(sql);
28430 let printed = s.to_string();
28431 let again = parse_statement(&printed)
28432 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28433 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28434 }
28435 }
28436
28437 #[test]
28438 fn subscription_ddl_display_roundtrips() {
28439 for sql in [
28440 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28441 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28442 "DROP SUBSCRIPTION sub_a",
28443 "SHOW SUBSCRIPTIONS",
28444 ] {
28445 let s = parse(sql);
28446 let printed = s.to_string();
28447 let again = parse_statement(&printed)
28448 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28449 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28450 }
28451 }
28452
28453 #[test]
28454 fn parser_drop_dispatches_user_vs_publication() {
28455 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28456 // tokenises DROP. Both targets must still parse.
28457 let s = parse("DROP USER 'alice'");
28458 let Statement::DropUser { name, .. } = s else {
28459 panic!("expected DropUser, got {s:?}")
28460 };
28461 assert_eq!(name, "alice");
28462 // And DROP PUBLICATION lands the new variant.
28463 let s = parse("DROP PUBLICATION p1");
28464 assert!(matches!(s, Statement::DropPublication { .. }));
28465 }
28466
28467 #[test]
28468 fn publication_ddl_display_roundtrips() {
28469 // Every CREATE PUBLICATION variant must Display → parse →
28470 // same AST. v6.1.3 covers all three scope shapes.
28471 for sql in [
28472 "CREATE PUBLICATION pub_a",
28473 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28474 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28475 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28476 "DROP PUBLICATION pub_a",
28477 "SHOW PUBLICATIONS",
28478 ] {
28479 let s = parse(sql);
28480 let printed = s.to_string();
28481 let again = parse_statement(&printed)
28482 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28483 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28484 }
28485 }
28486
28487 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28488
28489 #[test]
28490 fn create_function_returns_trigger_plpgsql_minimal() {
28491 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28492 let s = parse(sql);
28493 let Statement::CreateFunction(f) = s else {
28494 panic!("expected CreateFunction");
28495 };
28496 assert_eq!(f.name, "noop");
28497 assert!(!f.or_replace);
28498 assert!(f.args.is_empty());
28499 assert!(matches!(f.returns, FunctionReturn::Trigger));
28500 assert_eq!(f.language, "plpgsql");
28501 let FunctionBody::PlPgSql(block) = f.body else {
28502 panic!("expected PlPgSql body");
28503 };
28504 assert_eq!(block.statements.len(), 1);
28505 assert!(matches!(
28506 block.statements[0],
28507 PlPgSqlStmt::Return(ReturnTarget::New)
28508 ));
28509 }
28510
28511 #[test]
28512 fn create_function_or_replace_with_assignment() {
28513 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28514 // RETURN NEW.
28515 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28516BEGIN
28517 NEW.search_vector := to_tsvector('english', NEW.subject);
28518 RETURN NEW;
28519END;
28520$$";
28521 let s = parse(sql);
28522 let Statement::CreateFunction(f) = s else {
28523 panic!("expected CreateFunction");
28524 };
28525 assert!(f.or_replace);
28526 let FunctionBody::PlPgSql(block) = &f.body else {
28527 panic!("expected PlPgSql body");
28528 };
28529 assert_eq!(block.statements.len(), 2);
28530 // First statement: NEW.search_vector := to_tsvector(...)
28531 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28532 panic!("expected Assign as first stmt");
28533 };
28534 match target {
28535 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28536 other => panic!("expected NEW.col, got {other:?}"),
28537 }
28538 // Second statement: RETURN NEW
28539 assert!(matches!(
28540 block.statements[1],
28541 PlPgSqlStmt::Return(ReturnTarget::New)
28542 ));
28543 }
28544
28545 #[test]
28546 fn create_trigger_after_insert_or_update() {
28547 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28548 let s = parse(sql);
28549 let Statement::CreateTrigger(t) = s else {
28550 panic!("expected CreateTrigger");
28551 };
28552 assert_eq!(t.name, "tg");
28553 assert_eq!(t.table, "messages");
28554 assert_eq!(t.timing, TriggerTiming::After);
28555 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28556 assert_eq!(t.for_each, TriggerForEach::Row);
28557 assert_eq!(t.function, "update_sv");
28558 }
28559
28560 #[test]
28561 fn create_trigger_before_delete_execute_procedure_alias() {
28562 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28563 let sql =
28564 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28565 let s = parse(sql);
28566 let Statement::CreateTrigger(t) = s else {
28567 panic!("expected CreateTrigger");
28568 };
28569 assert_eq!(t.timing, TriggerTiming::Before);
28570 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28571 }
28572
28573 #[test]
28574 fn drop_trigger_if_exists_round_trips() {
28575 // No parser support for DROP TRIGGER yet — added in v7.12.5
28576 // alongside the broader DROP …{IF EXISTS} cleanup. The
28577 // AST + Display impls are in place so we round-trip via
28578 // construction:
28579 let s = Statement::DropTrigger {
28580 name: "tg".into(),
28581 table: "messages".into(),
28582 if_exists: true,
28583 };
28584 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28585 }
28586
28587 #[test]
28588 fn trigger_ddl_display_roundtrips_through_parser() {
28589 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28590 // Display → parse → same AST (modulo PL/pgSQL body
28591 // formatting which is parser-canonicalised).
28592 for sql in [
28593 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28594 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28595 ] {
28596 let s = parse(sql);
28597 let printed = s.to_string();
28598 let again = parse_statement(&printed)
28599 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28600 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28601 }
28602 }
28603}