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 // PG psql backslash meta-commands that newer
73 // pg_dump versions emit unescaped (\restrict /
74 // \unrestrict). Real psql intercepts these; SPG's
75 // PG-wire sees them as raw text.
76 | "\\restrict"
77 | "\\unrestrict"
78 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
79 // `DELIMITER ;` directives. Technically client-side
80 // (the `mysql` CLI uses them to set the statement
81 // terminator), not SQL — but mysqldump and stored-
82 // procedure scripts emit them inline. SPG's parser
83 // sees one statement at a time and doesn't care
84 // about the terminator, so consume DELIMITER lines
85 // as Empty.
86 | "delimiter"
87 // v7.37.17 (17.6 siblings) — additional PG maintenance /
88 // session-state statements pg_dump + application startup
89 // scripts emit. SPG has no matching session-state to
90 // discard (no prepared-plan cache surface, no temp
91 // sequences), no matching security-label / storage-
92 // option to apply, no separate CREATE/DROP CAST that
93 // affects execution.
94 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
95 // pg_dump / pg_dumpall emit around DROP ROLE:
96 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
97 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
98 // Both operate on the role's owned objects; SPG has no
99 // role-owner model, so accept-and-no-op.
100 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
101 // + extension scripts use LOAD to preload shared
102 // libraries. SPG doesn't have a shared-library extension
103 // point today (extensions ship as first-class crates
104 // linked at build time); accept as a no-op.
105 | "load"
106 )
107}
108
109/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
110/// per `pg_get_keywords()`. SPG tokenizes these as named variants
111/// so the parser can dispatch on them in their owning contexts
112/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
113/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
114/// column / alias names — that's the PG contract for unreserved
115/// keywords (see PG docs Appendix C.1).
116///
117/// Before this generalisation, sentori migration 0001_init.sql
118/// `release TEXT NOT NULL` blew up the parser with "expected
119/// identifier, got Release", and the same gap stalked every
120/// SPG drop-in user whose schema had a column / alias named
121/// `release` / `index` / `tables` / `show` / `savepoint` /
122/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
123/// / `limit` / `partition`. PG accepts all of them as identifiers
124/// when unquoted, so SPG must too.
125///
126/// Returns the canonical lowercase identifier text when the token
127/// belongs to PG's unreserved class, `None` otherwise. Used by
128/// `expect_ident_like` (column / table / alias names) so the
129/// generalisation applies everywhere an identifier may appear,
130/// not just in the contexts these tokens were introduced for.
131fn unreserved_keyword_text(tok: &Token) -> Option<String> {
132 let s = match tok {
133 // PG keyword class: unreserved or col_name.
134 //
135 Token::Release => "release",
136 Token::Savepoint => "savepoint",
137 Token::Show => "show",
138 Token::Index => "index",
139 Token::Begin => "begin",
140 Token::Commit => "commit",
141 Token::Rollback => "rollback",
142 Token::Drop => "drop",
143 Token::Insert => "insert",
144 Token::Values => "values",
145 Token::Limit => "limit",
146 Token::Partition => "partition",
147 Token::Tables => "tables",
148 Token::Connection => "connection",
149 Token::Publication => "publication",
150 Token::Subscription => "subscription",
151 Token::Interval => "interval",
152 // `extract` is non-reserved in PG too (it's a function the
153 // parser dispatches via context — outside that context it's
154 // a plain identifier).
155 Token::Extract => "extract",
156 Token::Offset => "offset",
157 // `to` is reserved in PG (used in many "AS … TO …" forms), so
158 // it is NOT relaxed here. Same for `from`, `where`, `as`,
159 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
160 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
161 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
162 // `left`, `cross`, `outer`, `default`, `is`, `between`,
163 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
164 // (partial — keep partition as unreserved per modern PG).
165 _ => return None,
166 };
167 Some(s.to_string())
168}
169
170/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
171/// in CREATE INDEX. SPG's HNSW already routes by query operator;
172/// the opclass is accepted for `pg_dump` compatibility (mailrs
173/// migration follow-up G5).
174/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
175/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
176/// doesn't change index behaviour based on them.
177/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
178/// FROM-clause pipeline; the stored name tells the executor whether
179/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
180/// or unwraps to text (`*_each_text`).
181fn is_json_each_name(s: &str) -> bool {
182 s.eq_ignore_ascii_case("jsonb_each_text")
183 || s.eq_ignore_ascii_case("jsonb_each")
184 || s.eq_ignore_ascii_case("json_each_text")
185 || s.eq_ignore_ascii_case("json_each")
186}
187
188/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
189/// to positional order for the `make_*` family (the AST stays positional).
190/// Positional args fill slots left-to-right; a named arg goes to its registered
191/// slot; unfilled slots default to integer 0 (PG's optional make_interval
192/// fields — the make_date/time arity is still checked at eval time).
193fn reorder_named_args(
194 fname: &str,
195 args: Vec<Expr>,
196 names: &[Option<String>],
197) -> Result<Vec<Expr>, String> {
198 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
199 "make_date" => &["year", "month", "day"],
200 "make_time" => &["hour", "min", "sec"],
201 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
202 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
203 other => {
204 return Err(alloc::format!(
205 "function {other}(...) does not support named arguments"
206 ));
207 }
208 };
209 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
210 let mut next_positional = 0usize;
211 for (arg, name) in args.into_iter().zip(names.iter()) {
212 let idx = match name {
213 Some(n) => params
214 .iter()
215 .position(|p| p.eq_ignore_ascii_case(n))
216 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
217 None => {
218 let i = next_positional;
219 next_positional += 1;
220 i
221 }
222 };
223 if idx >= slots.len() {
224 return Err(alloc::format!("too many arguments for {fname}(...)"));
225 }
226 if slots[idx].is_some() {
227 return Err(alloc::format!(
228 "argument \"{}\" specified more than once",
229 params[idx]
230 ));
231 }
232 slots[idx] = Some(arg);
233 }
234 Ok(slots
235 .into_iter()
236 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
237 .collect())
238}
239
240/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
241/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
242/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
243/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
244/// scientific literal into PG's plain NUMERIC decimal form.
245#[derive(Debug)]
246pub enum SciExpanded {
247 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
248 Expanded(String),
249 /// Exponent pushes the value outside PG's numeric format
250 /// (more than 131072 integer digits or 16383 fractional digits).
251 Overflow,
252 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
253 NotScientific,
254}
255
256/// Expand scientific notation into a plain decimal string by moving the
257/// decimal point — no float round-trip, so the value stays exact. PG treats
258/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
259/// limits ("value overflows numeric format").
260pub fn expand_scientific_literal(s: &str) -> SciExpanded {
261 let s = s.trim();
262 let Some(epos) = s.find(['e', 'E']) else {
263 return SciExpanded::NotScientific;
264 };
265 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
266 let Ok(exp) = exp_str.parse::<i64>() else {
267 return SciExpanded::NotScientific;
268 };
269 let (neg, mant) = match mant.strip_prefix('-') {
270 Some(r) => (true, r),
271 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
272 };
273 let (int_part, frac_part) = match mant.split_once('.') {
274 Some((i, f)) => (i, f),
275 None => (mant, ""),
276 };
277 if (int_part.is_empty() && frac_part.is_empty())
278 || !int_part.bytes().all(|b| b.is_ascii_digit())
279 || !frac_part.bytes().all(|b| b.is_ascii_digit())
280 {
281 return SciExpanded::NotScientific;
282 }
283 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
284 digits.push_str(int_part);
285 digits.push_str(frac_part);
286 // Decimal point position within `digits` after applying the exponent.
287 let new_point = int_part.len() as i64 + exp;
288 // PG's numeric format: up to 131072 digits before the point, 16383 after.
289 if new_point > 131_072 {
290 return SciExpanded::Overflow;
291 }
292 if (digits.len() as i64 - new_point) > 16_383 {
293 return SciExpanded::Overflow;
294 }
295 let sign = if neg { "-" } else { "" };
296 let plain = if new_point <= 0 {
297 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
298 out.push_str("0.");
299 for _ in 0..(-new_point) {
300 out.push('0');
301 }
302 out.push_str(&digits);
303 out
304 } else if (new_point as usize) >= digits.len() {
305 let mut out = digits;
306 for _ in 0..(new_point as usize - out.len()) {
307 out.push('0');
308 }
309 out
310 } else {
311 let mut out = String::with_capacity(digits.len() + 1);
312 out.push_str(&digits[..new_point as usize]);
313 out.push('.');
314 out.push_str(&digits[new_point as usize..]);
315 out
316 };
317 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
318}
319
320/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
321/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
322/// are left-padded to an even count (`0x123` → byte string `01 23`, per
323/// MariaDB) and handed to the PG bytea input format (`\x…`).
324#[inline(never)]
325fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
326 let padded = if hex.len() % 2 == 1 {
327 alloc::format!("0{hex}")
328 } else {
329 hex.to_string()
330 };
331 Expr::Cast {
332 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
333 "\\x{padded}"
334 )))),
335 target: CastTarget::Named("bytea".to_string()),
336 }
337}
338
339/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
340/// onto the bytea cast. The bits are read big-endian and left-padded to a
341/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
342#[inline(never)]
343fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
344 let pad = (8 - bits.len() % 8) % 8;
345 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
346 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
347 for nibble in padded.as_bytes().chunks(4) {
348 let mut v = 0u8;
349 for &b in nibble {
350 v = (v << 1) | (b - b'0');
351 }
352 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
353 }
354 hex_literal_to_bytea_expr(&hex)
355}
356
357/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
358/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
359/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
360/// scale width (u8) falls back to double precision.
361///
362/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
363/// against PG 18.4: a literal with 300 fractional digits round-trips
364/// identically on both engines, so whatever the note described is gone.
365/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
366/// measurement" rather than under "open".
367/// Kept out of the parse_expr recursion frame — see the call site.
368#[inline(never)]
369fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
370 match parse_decimal_literal(&s) {
371 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
372 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
373 // its exact value as a NumericBig.
374 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
375 // v7.39 (read01 numeric.c) — expand the exponent form.
376 None => match expand_scientific_literal(&s) {
377 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
378 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
379 None if plain
380 .split_once('.')
381 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
382 {
383 Ok(Literal::NumericBig(plain))
384 }
385 None => s
386 .parse::<f64>()
387 .map(Literal::Float)
388 .map_err(|_| format!("invalid numeric literal {s:?}")),
389 },
390 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
391 SciExpanded::NotScientific => s
392 .parse::<f64>()
393 .map(Literal::Float)
394 .map_err(|_| format!("invalid numeric literal {s:?}")),
395 },
396 }
397}
398
399fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
400 let (int_part, frac_part) = match s.split_once('.') {
401 Some((i, f)) => (i, f),
402 None => (s, ""),
403 };
404 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
405 // places fell out of the numeric path here, which is why
406 // `pg_typeof(1e-256)` answered double precision and a plain
407 // 256-place decimal aborted the query in the big-decimal converter.
408 if frac_part.len() > u16::MAX as usize {
409 return None;
410 }
411 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
412 digits.push_str(int_part);
413 digits.push_str(frac_part);
414 let mantissa: i128 = digits.parse().ok()?;
415 #[allow(clippy::cast_possible_truncation)]
416 Some((mantissa, frac_part.len() as u16))
417}
418
419/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
420/// record-returning JSON functions that take a `AS alias(col type, …)`
421/// column-definition list in FROM position.
422fn is_json_to_record_name(s: &str) -> bool {
423 s.eq_ignore_ascii_case("jsonb_to_recordset")
424 || s.eq_ignore_ascii_case("jsonb_to_record")
425 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
426 // column-definition list desugars identically (the record base
427 // argument only carries the type; a non-NULL base's field
428 // defaults are a recorded delta, RD-6).
429 || s.eq_ignore_ascii_case("json_populate_record")
430 || s.eq_ignore_ascii_case("jsonb_populate_record")
431 || s.eq_ignore_ascii_case("json_populate_recordset")
432 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
433 || s.eq_ignore_ascii_case("json_to_recordset")
434 || s.eq_ignore_ascii_case("json_to_record")
435}
436
437impl Parser {
438 /// Whether what follows an identifier ends an index key, which is how
439 /// an operator class is told from anything else in that position.
440 fn opclass_position_follows(next: Option<&Token>) -> bool {
441 match next {
442 // `ASC` / `DESC` have their own tokens; matching them as
443 // identifiers named "asc" / "desc" — which the first version of
444 // this did — never fires, and `(c text_pattern_ops DESC)` (which
445 // PG18.4 accepts, verified) went on failing to parse.
446 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
447 Some(Token::Ident(w)) => {
448 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
449 }
450 _ => false,
451 }
452 }
453}
454
455fn is_vector_opclass_name(name: &str) -> bool {
456 let lc = name.to_ascii_lowercase();
457 matches!(
458 lc.as_str(),
459 "vector_cosine_ops"
460 | "vector_l2_ops"
461 | "vector_ip_ops"
462 | "halfvec_cosine_ops"
463 | "halfvec_l2_ops"
464 | "halfvec_ip_ops"
465 | "sq8_cosine_ops"
466 | "sq8_l2_ops"
467 | "sq8_ip_ops"
468 // pg_trgm — trigram operator class. SPG's GIN index
469 // already uses tsvector tokens; trigram-style LIKE
470 // pattern matching still routes through a sequential
471 // scan, but the opclass name is accepted so PG schemas
472 // load.
473 | "gin_trgm_ops"
474 | "gist_trgm_ops"
475 // PG built-in btree opclasses occasionally appear in
476 // pg_dump output for column types with multiple
477 // sort orders (text_pattern_ops, varchar_pattern_ops,
478 // bpchar_pattern_ops).
479 | "text_pattern_ops"
480 | "varchar_pattern_ops"
481 | "bpchar_pattern_ops"
482 | "int4_ops"
483 | "int8_ops"
484 | "text_ops"
485 )
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct ParseError {
490 pub message: String,
491 /// Index into the token stream where parsing tripped. Not a byte offset.
492 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
493 /// field would grow every `Result<_, ParseError>` slot on the deeply
494 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
495 /// 1-based char position is recovered on the cold error path by
496 /// [`syntax_error_position`], which re-tokenizes to map this token index.
497 pub token_pos: usize,
498}
499
500impl fmt::Display for ParseError {
501 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
502 /// with `parse error at token #N: `, which PG has no equivalent of:
503 /// the message bodies are already PG's verbatim (`LIMIT must not be
504 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
505 /// prefix was SPG's internal token index leaking into every one of
506 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
507 /// character position from it for the ErrorResponse `P`.
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 f.write_str(&self.message)
510 }
511}
512
513impl From<LexError> for ParseError {
514 fn from(e: LexError) -> Self {
515 Self {
516 message: format!("lex: {e}"),
517 token_pos: 0,
518 }
519 }
520}
521
522/// v7.9.30 — parse a single expression (no trailing junk). Used by
523/// the engine to re-hydrate stored partial-index / unique-index
524/// predicates from their canonical Display form. The same Pratt
525/// parser the statement path uses; this entry point just skips the
526/// statement dispatch.
527pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
528 let (tokens, offsets) = lexer::tokenize_with_offsets(input, lexer::Dialect::PG)
529 .map_err(|e| shape_lex_error(&e, input))?;
530 let mut p = Parser::new(tokens);
531 let expr = p
532 .parse_expr(0)
533 .and_then(|e| p.expect_eof().map(|()| e))
534 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
535 Ok(expr)
536}
537
538/// Parse exactly one statement, swallow an optional trailing `;`, and require
539/// the token stream to end there. PG string semantics.
540pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
541 parse_statement_with(input, lexer::Dialect::PG)
542}
543
544/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
545/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
546/// The engine threads its session flag through here.
547pub fn parse_statement_with(input: &str, dialect: lexer::Dialect) -> Result<Statement, ParseError> {
548 let (tokens, offsets, merges) =
549 lexer::tokenize_with_merges(input, dialect).map_err(|e| shape_lex_error(&e, input))?;
550 // v7.39.2 — the grammar follows "is this MySQL", the lexer follows
551 // "does backslash escape". They used to be one flag, and a session
552 // that turned escapes off lost the grammar with them.
553 let mut p = Parser::new_with_dialect(tokens, dialect.speaks_mysql)
554 .with_source(input, &offsets)
555 .with_merges(merges);
556 let stmt = (|| {
557 let stmt = p.parse_one_statement()?;
558 if matches!(p.peek(), Token::Semicolon) {
559 p.advance();
560 }
561 p.expect_eof()?;
562 Ok(stmt)
563 })()
564 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
565 Ok(stmt)
566}
567
568/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
569/// `syntax error at or near "<token>"` and `syntax error at end of input`
570/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
571/// prose — `expected identifier, got Eof`, `unexpected token From in
572/// expression`, `expected end of input, got Ident("with")` — which named
573/// internal token types and, in the Debug forms, leaked the parser's own
574/// enum into a message clients read.
575///
576/// Applied once on the way out, so every construction site is covered and
577/// the token named is the one the error itself points at. Messages whose
578/// bodies are already PG's verbatim (`LIMIT must not be negative`,
579/// `invalid input syntax for type bigint: "abc"`) are left alone — those
580/// are PG's own errors, not its syntax error.
581fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
582 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
583 return e;
584 }
585 let message = match offending_lexeme(input, offsets, e.token_pos) {
586 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
587 None => "syntax error at end of input".into(),
588 };
589 ParseError {
590 message,
591 token_pos: e.token_pos,
592 }
593}
594
595/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
596/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
597/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
598/// comment at or near "/* x"` — the quoted part runs from the opening
599/// delimiter to the end of the input. SPG reported its own internal
600/// shape instead (`unterminated string literal at byte 7`), which named
601/// a byte offset no client can use.
602fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
603 use lexer::LexErrorKind as K;
604 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
605 let message = match &e.kind {
606 K::UnterminatedString => {
607 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
608 }
609 K::UnterminatedQuotedIdent => {
610 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
611 }
612 K::UnterminatedBlockComment => {
613 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
614 }
615 // PG has no "unknown character" error of its own — the character
616 // is skipped and the parser reports the next token. SPG stops at
617 // the character itself and names it, which is the same shape.
618 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
619 // The number-literal kinds already carry PG's `at or near` form.
620 other => alloc::format!(
621 "{}",
622 lexer::LexError {
623 kind: other.clone(),
624 pos: e.pos,
625 }
626 ),
627 };
628 ParseError {
629 message,
630 token_pos: 0,
631 }
632}
633
634/// The offending token exactly as it appears in the input, or `None` at
635/// end of input. PG echoes the source spelling — a lower-case `frm`
636/// reports as `frm`, not as a canonicalised keyword.
637fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
638 let start = *offsets.get(token_pos)?;
639 if start >= input.len() {
640 return None;
641 }
642 let end = offsets
643 .get(token_pos + 1)
644 .copied()
645 .unwrap_or(input.len())
646 .min(input.len());
647 let seg = input.get(start..end)?.trim();
648 if seg.is_empty() {
649 return None;
650 }
651 // A quoted literal / identifier keeps its inner spaces; anything else
652 // ends at the first whitespace (the segment runs to the NEXT token's
653 // start, which may swallow a comment).
654 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
655 Some(seg)
656 } else {
657 seg.split_whitespace().next()
658 }
659}
660
661/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
662/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
663/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
664/// this re-tokenizes `input` on the cold error path to map the failing token
665/// index to its start byte, then to a character offset. The dialect
666/// must match the parse that produced `token_pos` (it barely shifts offsets,
667/// but stay consistent). Returns `None` when the index has no offset or the
668/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
669#[must_use]
670pub fn syntax_error_position(
671 input: &str,
672 dialect: lexer::Dialect,
673 token_pos: usize,
674) -> Option<usize> {
675 let (_, offsets) = lexer::tokenize_with_offsets(input, dialect).ok()?;
676 let byte_off = *offsets.get(token_pos)?;
677 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
678 return None;
679 }
680 Some(input[..byte_off].chars().count() + 1)
681}
682
683struct Parser {
684 tokens: Vec<Token>,
685 pos: usize,
686 /// v7.39 (round 274) — the session's dialect, carried by the same
687 /// signal that drives string-literal escaping: `SET sql_mode` (only
688 /// MySQL clients and mysqldump preambles emit it) turns it on,
689 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
690 /// it off. Needed here because the two dialects disagree about what
691 /// `REAL` means — see the type mapping below.
692 mysql_dialect: bool,
693 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
694 /// mutually recursive expr/select parsers. Bounded so a deeply
695 /// nested input returns a parse error instead of overflowing
696 /// the stack (embed hosts die on overflow — it is an abort,
697 /// not a catchable error).
698 nest_depth: usize,
699 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
700 /// `random() < p/100` predicate here; the enclosing SELECT
701 /// drains the list after its WHERE parses and ANDs the
702 /// predicates in. parse_bare_select save/restores around its
703 /// FROM+WHERE so nested selects only drain their own.
704 pending_sample_preds: Vec<Expr>,
705 /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
706 /// of `parse_bare_select` (which returns a `SelectStatement` and has
707 /// nowhere to put it) to the caller that lowers the pair to the CTAS
708 /// node. `bool` is `TEMP`.
709 pending_select_into: Option<(String, bool)>,
710 /// v7.39 (round 691) — collation lowering channel, the same shape as
711 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
712 /// information, and `ast::OrderBy` is where this parser keeps ordering
713 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
714 /// variant — puts a new arm on `eval_expr`, which this repo has
715 /// measured to overflow the debug stack. So while an ORDER BY KEY is
716 /// being parsed the postfix loop drops the name here instead of
717 /// refusing it, and the key's parser takes it.
718 ///
719 /// Only inside an ORDER BY key: everywhere else an unperformable
720 /// collation still errors, because accepting one at a COMPARISON and
721 /// ignoring it is the defect F36 exists to close.
722 in_order_by_key: bool,
723 order_key_collation: Option<String>,
724 /// POSITION(sub IN str) — while parsing the needle, the IN
725 /// keyword is the argument separator, not a membership test.
726 /// The postfix loop leaves IN unconsumed when this is set.
727 suppress_in_tail: bool,
728 /// Index of the token the last `advance()` returned — see
729 /// [`Parser::consumed_pos`].
730 last_consumed: usize,
731 /// v7.39 (round 506) — the statement's own text and the byte each token
732 /// starts at, so a MySQL projection item can report the SOURCE TEXT
733 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
734 /// spacing and all. Only filled for a MySQL session — a PG one names
735 /// columns from the parsed shape and pays nothing for this.
736 src: Option<(String, Vec<usize>)>,
737 /// v7.39.3 — (token index, first-segment byte length) for every
738 /// string literal the lexer built by implicit concatenation.
739 merges: Vec<(usize, usize)>,
740}
741
742/// Max expr/select parser nesting (parens, subqueries, CASE, …).
743/// Real SQL nests a few dozen levels at the extreme. Each nesting level
744/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
745/// exists to turn a deep statement into a catchable parse ERROR: a stack
746/// overflow is an abort, and in the server it does not fail one query, it
747/// takes the process down and every other connection with it.
748///
749/// v7.39 (round 507) — measured, because the figure here used to be a
750/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
751/// in BOTH debug and release"), and the debug half of that is wrong by
752/// more than an order of magnitude:
753///
754/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
755/// this budget and errors. Verified against a live server for nested
756/// derived tables, parens, calls, CASE, IN-subqueries, scalar
757/// subqueries, NOT and unary minus — the server stayed up through all
758/// of them. This is the contract that matters, and it holds.
759/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
760/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
761/// and executing aborts around 8 inside a test thread. The budget is
762/// simply unreachable there, which is why a deep-nesting test has to
763/// ask for a large stack of its own — see `nesting_budget_errors_at`
764/// in the parser tests.
765/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
766/// one place.
767///
768/// There were two copies of this fact: a curated list, used for BARE
769/// names, and — in `try_peek_meta_qualified` — no list at all, which
770/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
771/// the engine to complain about a view it could not materialise. So
772/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
773/// had rows, `pg_catalog.pg_stat_activity` was an error.
774///
775/// PG puts `pg_catalog` at the implicit front of every search_path, so
776/// the two spellings name the same relation and must resolve the same
777/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
778/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
779/// meta_view_result path under their own names and must not be
780/// rewritten; a name that is neither reaches the ordinary resolver,
781/// which reports that the relation does not exist — PG's answer.
782const SYNTHESISED_PG_CATALOGS: &[&str] = &[
783 "pg_am",
784 "pg_attrdef",
785 "pg_attribute",
786 "pg_cast",
787 "pg_db_role_setting",
788 "pg_conversion",
789 "pg_default_acl",
790 "pg_shadow",
791 "pg_sequences",
792 "pg_range",
793 "pg_partitioned_table",
794 "pg_language",
795 "pg_group",
796 "pg_authid",
797 "pg_class",
798 "pg_collation",
799 "pg_constraint",
800 "pg_database",
801 "pg_depend",
802 "pg_amop",
803 "pg_amproc",
804 "pg_opclass",
805 "pg_opfamily",
806 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
807 "pg_description",
808 "pg_enum",
809 "pg_extension",
810 // v7.39 (round 541) — pg_dump reads it for every relation of kind
811 // 'f'. SPG has no foreign tables, so it is empty, which is also
812 // what PG reports on a database that has none.
813 "pg_foreign_table",
814 // v7.39 (round 541) — the empty-by-truth family; see
815 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
816 "pg_event_trigger",
817 "pg_file_settings",
818 "pg_foreign_data_wrapper",
819 "pg_foreign_server",
820 "pg_hba_file_rules",
821 "pg_ident_file_mappings",
822 "pg_init_privs",
823 "pg_parameter_acl",
824 "pg_prepared_xacts",
825 "pg_publication_namespace",
826 "pg_publication_rel",
827 "pg_publication_tables",
828 "pg_replication_origin",
829 "pg_replication_origin_status",
830 "pg_seclabel",
831 "pg_seclabels",
832 "pg_shdepend",
833 "pg_shdescription",
834 "pg_shmem_allocations",
835 "pg_shmem_allocations_numa",
836 "pg_shseclabel",
837 "pg_statistic_ext_data",
838 "pg_stats_ext",
839 "pg_stats_ext_exprs",
840 "pg_subscription_rel",
841 "pg_transform",
842 "pg_user_mapping",
843 "pg_user_mappings",
844 "pg_index",
845 "pg_indexes",
846 "pg_inherits",
847 // v7.39 (round 650) — the text-search catalogs SPG can fill
848 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
849 // token types to dictionaries and SPG has no token-type model,
850 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
851 "pg_ts_config",
852 "pg_ts_config_map",
853 "pg_ts_dict",
854 "pg_ts_parser",
855 "pg_ts_template",
856 "pg_matviews",
857 "pg_namespace",
858 // v7.39 (round 621)
859 "pg_operator",
860 "pg_policies",
861 "pg_policy",
862 "pg_proc",
863 "pg_publication",
864 "pg_replication_slots",
865 "pg_roles",
866 // v7.39 (round 143) — the rewrite-rule listing view.
867 // v7.39 (round 312) — and the rule catalogue itself, which
868 // `pg_get_ruledef(oid)` resolves against.
869 "pg_rewrite",
870 "pg_rules",
871 "pg_sequence",
872 "pg_settings",
873 "pg_stat_archiver",
874 "pg_stat_bgwriter",
875 "pg_stat_checkpointer",
876 "pg_stat_database",
877 "pg_stat_io",
878 "pg_stat_progress_analyze",
879 "pg_auth_members",
880 "pg_stat_progress_create_index",
881 "pg_stat_progress_vacuum",
882 "pg_stat_replication",
883 "pg_stat_slru",
884 "pg_stat_subscription_stats",
885 "pg_stat_user_functions",
886 "pg_stat_user_indexes",
887 "pg_stat_user_tables",
888 "pg_stat_wal",
889 "pg_prepared_statements",
890 "pg_largeobject",
891 "pg_largeobject_metadata",
892 "pg_statistic",
893 "pg_statistic_ext",
894 // v7.38.18 — the readable view over pg_statistic.
895 "pg_stats",
896 "pg_subscription",
897 "pg_tables",
898 "pg_tablespace",
899 // v7.39 (round 502) — the timezone catalogues. SPG resolved
900 // named zones correctly but could not list them, so a client
901 // populating a timezone picker got "relation does not exist".
902 "pg_timezone_abbrevs",
903 "pg_timezone_names",
904 "pg_trigger",
905 "pg_type",
906 "pg_user",
907 "pg_views",
908];
909
910const MAX_NEST_DEPTH: usize = 64;
911
912/// Stack accounting for the nesting budget, test-only.
913///
914/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
915/// that MOVES: a compiler upgrade grew the parser's debug frames and
916/// silently ate the margin until `nesting_budget_errors_cleanly` went
917/// from erroring cleanly to aborting on a stack overflow. A count
918/// cannot notice that on its own, so the budget is measured here and
919/// held to a ceiling.
920///
921/// The reading has to come from a helper whose OWN frame is the same at
922/// every call: debug slot placement does not follow source order, so a
923/// local's address inside the function under test is not that
924/// function's frame boundary. Two earlier probes were wrong that way —
925/// one read `&self.nest_depth`, which is the `Parser`'s address and
926/// never moves at all.
927#[cfg(test)]
928mod frame_meter {
929 extern crate std;
930 use std::cell::Cell;
931
932 // Per-THREAD, not global. `cargo test` runs tests in parallel and
933 // plenty of them parse nested expressions, so shared statics get
934 // stack addresses from several threads at once and the subtraction
935 // below turns into noise — it read 229,772 bytes per level that way,
936 // while passing when the test was run on its own.
937 std::thread_local! {
938 static AT_LO: Cell<usize> = const { Cell::new(0) };
939 static AT_HI: Cell<usize> = const { Cell::new(0) };
940 }
941
942 pub(super) const SAMPLE_LO: usize = 4;
943 pub(super) const SAMPLE_HI: usize = 24;
944
945 #[inline(never)]
946 pub(super) fn record(depth: usize) {
947 let anchor = 0u8;
948 let at = core::ptr::from_ref(&anchor) as usize;
949 if depth == SAMPLE_LO {
950 AT_LO.with(|c| c.set(at));
951 } else if depth == SAMPLE_HI {
952 AT_HI.with(|c| c.set(at));
953 }
954 }
955
956 /// Bytes of stack one nesting level costs, averaged over the span.
957 pub(super) fn bytes_per_level() -> usize {
958 let lo = AT_LO.with(Cell::get);
959 let hi = AT_HI.with(Cell::get);
960 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
961 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
962 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
963 }
964
965 pub(super) fn reset() {
966 AT_LO.with(|c| c.set(0));
967 AT_HI.with(|c| c.set(0));
968 }
969}
970
971/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
972/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
973#[inline(never)]
974fn build_center_call(e: Expr) -> Expr {
975 Expr::FunctionCall {
976 name: alloc::string::String::from("center"),
977 args: alloc::vec![e],
978 }
979}
980
981/// Max consecutive binary operators at ONE precedence level
982/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
983/// parse time but evaluates and drops recursively — depth beyond
984/// this overflows 2 MiB worker stacks (debug eval frames run
985/// multiple KiB). `IN (…)` lists are flat and unaffected.
986const MAX_BINARY_CHAIN: usize = 256;
987
988/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
989/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
990/// it keeps its dedicated path (`parse_table_level_fk`).
991enum NamedTableConstraintKind {
992 Check,
993 Unique,
994 PrimaryKey,
995 Exclude,
996}
997
998impl Parser {
999 fn new(tokens: Vec<Token>) -> Self {
1000 Self::new_with_dialect(tokens, false)
1001 }
1002
1003 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
1004 Self {
1005 tokens,
1006 mysql_dialect,
1007 in_order_by_key: false,
1008 order_key_collation: None,
1009 pos: 0,
1010 nest_depth: 0,
1011 pending_sample_preds: Vec::new(),
1012 pending_select_into: None,
1013 suppress_in_tail: false,
1014 last_consumed: 0,
1015 src: None,
1016 merges: Vec::new(),
1017 }
1018 }
1019
1020 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1021 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1022 if self.mysql_dialect {
1023 self.src = Some((input.to_string(), offsets.to_vec()));
1024 }
1025 self
1026 }
1027
1028 /// v7.39.3 — the implicit-concatenation log from the lexer, so a
1029 /// merged literal can still be LABELLED by its first segment the way
1030 /// MySQL 9.7.2 labels it.
1031 fn with_merges(mut self, merges: Vec<(usize, usize)>) -> Self {
1032 if self.mysql_dialect {
1033 self.merges = merges;
1034 }
1035 self
1036 }
1037
1038 /// The byte length of the first segment of the literal at `tok`, when
1039 /// that literal was built by implicit concatenation.
1040 fn merged_first_len(&self, tok: usize) -> Option<usize> {
1041 self.merges
1042 .iter()
1043 .find(|(k, _)| *k == tok)
1044 .map(|(_, len)| *len)
1045 }
1046
1047 /// The source text spanning tokens `start ..= end`, trimmed.
1048 ///
1049 /// The offsets are token STARTS, so the span runs to the start of the
1050 /// token after `end` and gives back the whitespace between them —
1051 /// trimming is what makes `a + b FROM t` end at `b`.
1052 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1053 let (text, offsets) = self.src.as_ref()?;
1054 let from = *offsets.get(start)?;
1055 let to = *offsets.get(end + 1)?;
1056 text.get(from..to).map(str::trim_end)
1057 }
1058
1059 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1060 /// nesting depth, erroring out cleanly past the budget.
1061 fn enter_nested(&mut self) -> Result<(), ParseError> {
1062 self.nest_depth += 1;
1063 #[cfg(test)]
1064 frame_meter::record(self.nest_depth);
1065 if self.nest_depth > MAX_NEST_DEPTH {
1066 self.nest_depth -= 1;
1067 return Err(self.err(alloc::format!(
1068 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1069 )));
1070 }
1071 Ok(())
1072 }
1073
1074 fn peek(&self) -> &Token {
1075 // tokens always ends with Eof; pos is clamped in advance().
1076 &self.tokens[self.pos]
1077 }
1078
1079 fn advance(&mut self) -> Token {
1080 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1081 self.last_consumed = self.pos;
1082 if self.pos + 1 < self.tokens.len() {
1083 self.pos += 1;
1084 }
1085 t
1086 }
1087
1088 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1089 /// returned. It was computed as `pos - 1`, which is wrong at both
1090 /// ends: `advance()` parks on the final Eof rather than running off
1091 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1092 /// input`), and after backtracking `pos` is no longer one past the
1093 /// token that failed. Recorded by `advance()` itself instead.
1094 const fn consumed_pos(&self) -> usize {
1095 self.last_consumed
1096 }
1097
1098 fn err(&self, message: String) -> ParseError {
1099 ParseError {
1100 message,
1101 token_pos: self.pos,
1102 }
1103 }
1104
1105 /// v7.39.3 — like [`Parser::err`] but pointing at a token the caller
1106 /// names rather than at the current one.
1107 ///
1108 /// The position is not decoration on the MySQL wire: its syntax-error
1109 /// sentence quotes the source from there to the end of the statement,
1110 /// so an error raised after the construct it is about quotes nothing.
1111 fn err_at(&self, token_pos: usize, message: String) -> ParseError {
1112 ParseError { message, token_pos }
1113 }
1114
1115 fn expect_eof(&self) -> Result<(), ParseError> {
1116 if matches!(self.peek(), Token::Eof) {
1117 Ok(())
1118 } else {
1119 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1120 }
1121 }
1122
1123 /// v7.14.0 — swallow every token up to (but not including) the
1124 /// next semicolon / EOF. Used by the dump-noise dispatcher
1125 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1126 /// etc. without modeling each grammar.
1127 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1128 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1129 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1130 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1131 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1132 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1133 let start = self.pos;
1134 self.advance(); // COMMENT
1135 if !matches!(self.peek(), Token::On) {
1136 self.pos = start;
1137 self.consume_until_statement_boundary();
1138 return Ok(Statement::Empty);
1139 }
1140 self.advance(); // ON
1141 let kind = match self.peek() {
1142 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1143 Token::Table => "table".into(),
1144 _ => {
1145 self.consume_until_statement_boundary();
1146 return Ok(Statement::Empty);
1147 }
1148 };
1149 if !matches!(
1150 kind.as_str(),
1151 "table"
1152 | "column"
1153 | "index"
1154 | "view"
1155 | "sequence"
1156 | "schema"
1157 | "type"
1158 | "database"
1159 | "function"
1160 ) {
1161 self.consume_until_statement_boundary();
1162 return Ok(Statement::Empty);
1163 }
1164 self.advance(); // the kind keyword
1165 // The object name. ⚠️ `expect_ident_like` strips a leading
1166 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1167 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1168 // `c`. Read the dotted parts from raw tokens instead, then let a
1169 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1170 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1171 loop {
1172 match self.advance() {
1173 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1174 other if unreserved_keyword_text(&other).is_some() => {
1175 parts.push(unreserved_keyword_text(&other).unwrap());
1176 }
1177 other => {
1178 return Err(ParseError {
1179 message: alloc::format!("expected identifier, got {other:?}"),
1180 token_pos: self.consumed_pos(),
1181 });
1182 }
1183 }
1184 if matches!(self.peek(), Token::Dot) {
1185 self.advance();
1186 } else {
1187 break;
1188 }
1189 }
1190 // COLUMN wants `table.column`; every other kind wants a bare name.
1191 let want = if kind == "column" { 2 } else { 1 };
1192 while parts.len() > want {
1193 parts.remove(0);
1194 }
1195 let name = parts.join(".");
1196 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1197 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1198 // error here — a dump carrying one function comment failed to
1199 // restore. The list is consumed (the comment store keys by name;
1200 // overload-precise comments are the function-predicate follow-up).
1201 if matches!(self.peek(), Token::LParen)
1202 && matches!(
1203 kind.as_str(),
1204 "function" | "procedure" | "aggregate" | "routine"
1205 )
1206 {
1207 let mut depth = 0usize;
1208 loop {
1209 match self.advance() {
1210 Token::LParen => depth += 1,
1211 Token::RParen => {
1212 depth -= 1;
1213 if depth == 0 {
1214 break;
1215 }
1216 }
1217 Token::Eof => {
1218 return Err(self.err(alloc::string::String::from(
1219 "unterminated argument list in COMMENT ON",
1220 )));
1221 }
1222 _ => {}
1223 }
1224 }
1225 }
1226 // `IS`
1227 if !matches!(self.peek(), Token::Is) {
1228 self.expect_keyword_ident("is")?;
1229 } else {
1230 self.advance();
1231 }
1232 let comment = match self.peek() {
1233 Token::Null => {
1234 self.advance();
1235 None
1236 }
1237 _ => Some(self.expect_string_literal()?),
1238 };
1239 Ok(Statement::CommentOn {
1240 kind,
1241 name,
1242 comment,
1243 })
1244 }
1245
1246 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1247 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1248 /// [CASCADE|RESTRICT]`.
1249 ///
1250 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1251 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1252 /// and the no-ON `GRANT role TO role` membership form — parses into
1253 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1254 /// on them still restores.
1255 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1256 self.advance(); // GRANT / REVOKE
1257 // REVOKE's optional `GRANT OPTION FOR` prefix.
1258 let mut grant_option = false;
1259 if !grant
1260 && self.peek_keyword_ident("grant")
1261 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1262 {
1263 self.advance(); // GRANT
1264 self.advance(); // OPTION
1265 self.expect_keyword_ident("for")?;
1266 grant_option = true;
1267 }
1268 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1269 // words each with an optional COLUMN list.
1270 let mut privileges: Vec<GrantPriv> = Vec::new();
1271 if matches!(self.peek(), Token::All) {
1272 self.advance();
1273 if self.peek_keyword_ident("privileges") {
1274 self.advance();
1275 }
1276 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1277 // column only.
1278 if matches!(self.peek(), Token::LParen) {
1279 let columns = self.parse_grant_column_list()?;
1280 privileges.push(GrantPriv {
1281 word: "ALL".into(),
1282 columns,
1283 });
1284 }
1285 } else {
1286 loop {
1287 // SELECT and INSERT lex as reserved tokens, so they never
1288 // reach `expect_ident_like` as plain idents; the rest
1289 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1290 // MAINTAIN) are ordinary identifiers.
1291 let w = match self.peek() {
1292 Token::Select => {
1293 self.advance();
1294 "SELECT".to_string()
1295 }
1296 Token::Insert => {
1297 self.advance();
1298 "INSERT".to_string()
1299 }
1300 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1301 // schema / database, and it lexes as a reserved token.
1302 Token::Create => {
1303 self.advance();
1304 "CREATE".to_string()
1305 }
1306 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1307 // alice`) these "privilege words" are ROLE NAMES, and a
1308 // role name is case-sensitive. `priv_from_word` folds case
1309 // itself when they really are privileges.
1310 _ => self.expect_ident_like()?,
1311 };
1312 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1313 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1314 let columns = if matches!(self.peek(), Token::LParen) {
1315 self.parse_grant_column_list()?
1316 } else {
1317 Vec::new()
1318 };
1319 privileges.push(GrantPriv { word: w, columns });
1320 if matches!(self.peek(), Token::Comma) {
1321 self.advance();
1322 } else {
1323 break;
1324 }
1325 }
1326 }
1327 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1328 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1329 if !matches!(self.peek(), Token::On) {
1330 let roles: Vec<String> = core::mem::take(&mut privileges)
1331 .into_iter()
1332 .map(|p| p.word)
1333 .collect();
1334 let grantees = self.parse_grantee_list(grant)?;
1335 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1336 // no admin-option layer: a member cannot re-grant).
1337 self.consume_until_statement_boundary();
1338 return Ok(finish_grant(
1339 grant,
1340 GrantStatement {
1341 privileges: Vec::new(),
1342 object: GrantObject::Roles(roles),
1343 grantees,
1344 grant_option,
1345 },
1346 ));
1347 }
1348 self.advance(); // ON
1349 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1350 // the enforced case; anything else parses and no-ops.
1351 let mut class = "TABLE";
1352 match self.peek() {
1353 Token::Table => {
1354 self.advance();
1355 }
1356 Token::All => {
1357 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1358 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1359 // IN SCHEMA` stay no-ops and keep their own object class.
1360 self.advance(); // ALL
1361 let kind = match self.peek() {
1362 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1363 // TABLES has its own token (SHOW TABLES owns it).
1364 Token::Tables | Token::Table => "tables".to_string(),
1365 _ => String::new(),
1366 };
1367 if !kind.is_empty() {
1368 self.advance();
1369 }
1370 // `IN SCHEMA <name>`
1371 if matches!(self.peek(), Token::In) {
1372 self.advance();
1373 if self.peek_keyword_ident("schema") {
1374 self.advance();
1375 let _schema = self.expect_ident_like()?;
1376 }
1377 }
1378 if kind != "tables" {
1379 self.consume_until_statement_boundary();
1380 return Ok(finish_grant(
1381 grant,
1382 GrantStatement {
1383 privileges,
1384 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1385 grantees: Vec::new(),
1386 grant_option,
1387 },
1388 ));
1389 }
1390 let grantees = self.parse_grantee_list(grant)?;
1391 self.consume_until_statement_boundary();
1392 return Ok(finish_grant(
1393 grant,
1394 GrantStatement {
1395 privileges,
1396 object: GrantObject::AllTablesInSchema,
1397 grantees,
1398 grant_option,
1399 },
1400 ));
1401 }
1402 Token::Ident(w) | Token::QuotedIdent(w) => {
1403 let lc = w.to_ascii_lowercase();
1404 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1405 // real objects with real ACLs now.
1406 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1407 self.advance();
1408 let mut names: Vec<String> = Vec::new();
1409 loop {
1410 let mut parts: Vec<String> = Vec::new();
1411 loop {
1412 parts.push(self.expect_ident_like()?);
1413 if matches!(self.peek(), Token::Dot) {
1414 self.advance();
1415 } else {
1416 break;
1417 }
1418 }
1419 names.push(parts.pop().expect("at least one part"));
1420 if matches!(self.peek(), Token::Comma) {
1421 self.advance();
1422 } else {
1423 break;
1424 }
1425 }
1426 let grantees = self.parse_grantee_list(grant)?;
1427 let mut grant_option = grant_option;
1428 if grant && self.peek_keyword_ident("with") {
1429 self.advance();
1430 self.expect_keyword_ident("grant")?;
1431 self.expect_keyword_ident("option")?;
1432 grant_option = true;
1433 }
1434 self.consume_until_statement_boundary();
1435 let object = match lc.as_str() {
1436 "sequence" => GrantObject::Sequences(names),
1437 "schema" => GrantObject::Schemas(names),
1438 _ => GrantObject::Databases(names),
1439 };
1440 return Ok(finish_grant(
1441 grant,
1442 GrantStatement {
1443 privileges,
1444 object,
1445 grantees,
1446 grant_option,
1447 },
1448 ));
1449 }
1450 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1451 // keys functions by NAME, so the argument list parses and is
1452 // dropped (an overload set shares one ACL — recorded residual).
1453 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1454 self.advance();
1455 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1456 loop {
1457 let mut parts: Vec<String> = Vec::new();
1458 loop {
1459 parts.push(self.expect_ident_like()?);
1460 if matches!(self.peek(), Token::Dot) {
1461 self.advance();
1462 } else {
1463 break;
1464 }
1465 }
1466 let fname = parts.pop().expect("at least one part");
1467 // v7.39 (read01 round 62) — the signature picks the
1468 // overload, so it is captured.
1469 let sig = if matches!(self.peek(), Token::LParen) {
1470 Some(self.parse_function_signature_types()?)
1471 } else {
1472 None
1473 };
1474 names.push((fname, sig));
1475 if matches!(self.peek(), Token::Comma) {
1476 self.advance();
1477 } else {
1478 break;
1479 }
1480 }
1481 let grantees = self.parse_grantee_list(grant)?;
1482 self.consume_until_statement_boundary();
1483 return Ok(finish_grant(
1484 grant,
1485 GrantStatement {
1486 privileges,
1487 object: GrantObject::Functions(names),
1488 grantees,
1489 grant_option,
1490 },
1491 ));
1492 }
1493 if matches!(
1494 lc.as_str(),
1495 "type"
1496 | "domain"
1497 | "language"
1498 | "tablespace"
1499 | "large"
1500 | "foreign"
1501 | "parameter"
1502 ) {
1503 self.consume_until_statement_boundary();
1504 return Ok(finish_grant(
1505 grant,
1506 GrantStatement {
1507 privileges,
1508 object: GrantObject::Other(lc.to_ascii_uppercase()),
1509 grantees: Vec::new(),
1510 grant_option,
1511 },
1512 ));
1513 }
1514 class = "TABLE";
1515 }
1516 _ => {}
1517 }
1518 let _ = class;
1519 // The table list. Schema-qualified names drop their qualifier (SPG is
1520 // single-schema) — but read the dotted parts from raw tokens, since
1521 // `expect_ident_like` would silently swallow the leading part.
1522 let mut tables: Vec<String> = Vec::new();
1523 loop {
1524 let mut parts: Vec<String> = Vec::new();
1525 loop {
1526 parts.push(self.expect_ident_like()?);
1527 if matches!(self.peek(), Token::Dot) {
1528 self.advance();
1529 } else {
1530 break;
1531 }
1532 }
1533 tables.push(parts.pop().expect("at least one part"));
1534 if matches!(self.peek(), Token::Comma) {
1535 self.advance();
1536 } else {
1537 break;
1538 }
1539 }
1540 let grantees = self.parse_grantee_list(grant)?;
1541 if grant && self.peek_keyword_ident("with") {
1542 self.advance();
1543 self.expect_keyword_ident("grant")?;
1544 self.expect_keyword_ident("option")?;
1545 grant_option = true;
1546 }
1547 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1548 // to cascade to (no re-granting), so both are accepted and ignored.
1549 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1550 self.advance();
1551 }
1552 Ok(finish_grant(
1553 grant,
1554 GrantStatement {
1555 privileges,
1556 object: GrantObject::Tables(tables),
1557 grantees,
1558 grant_option,
1559 },
1560 ))
1561 }
1562
1563 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1564 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1565 /// words; the caller normalises them into a signature key.
1566 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1567 self.advance(); // (
1568 let mut types: Vec<String> = Vec::new();
1569 if matches!(self.peek(), Token::RParen) {
1570 self.advance();
1571 return Ok(types);
1572 }
1573 loop {
1574 // Collect the words of one argument up to a comma / close paren.
1575 let mut words: Vec<String> = Vec::new();
1576 loop {
1577 match self.peek() {
1578 Token::Comma | Token::RParen | Token::Eof => break,
1579 _ => {}
1580 }
1581 let tok = self.advance();
1582 match tok {
1583 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1584 other => {
1585 if let Some(w) = unreserved_keyword_text(&other) {
1586 words.push(w);
1587 }
1588 }
1589 }
1590 }
1591 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1592 // themselves several words (`double precision`, `character
1593 // varying`, `timestamp with time zone`), so "two words means the
1594 // first is a parameter name" reads the type off `f(double
1595 // precision)` as `precision`. v7.39 (round 282): recognise the
1596 // multi-word spellings first — a leading word that STARTS one of
1597 // them is part of the type, not a name.
1598 let joined = words.join(" ");
1599 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1600 joined
1601 } else if words.len() >= 2 {
1602 words[1..].join(" ")
1603 } else {
1604 words.first().cloned().unwrap_or_default()
1605 };
1606 types.push(ty);
1607 if matches!(self.peek(), Token::Comma) {
1608 self.advance();
1609 } else {
1610 break;
1611 }
1612 }
1613 if matches!(self.peek(), Token::RParen) {
1614 self.advance();
1615 }
1616 Ok(types)
1617 }
1618
1619 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1620 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1621 self.advance(); // (
1622 let mut cols = Vec::new();
1623 loop {
1624 cols.push(self.expect_ident_like()?);
1625 if matches!(self.peek(), Token::Comma) {
1626 self.advance();
1627 } else {
1628 break;
1629 }
1630 }
1631 if !matches!(self.peek(), Token::RParen) {
1632 return Err(self.err(alloc::format!(
1633 "expected ')' to close the column list, got {:?}",
1634 self.peek()
1635 )));
1636 }
1637 self.advance(); // )
1638 Ok(cols)
1639 }
1640
1641 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1642 /// PUBLIC.
1643 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1644 if grant {
1645 if matches!(self.peek(), Token::To) {
1646 self.advance();
1647 } else {
1648 self.expect_keyword_ident("to")?;
1649 }
1650 } else if matches!(self.peek(), Token::From) {
1651 self.advance();
1652 } else {
1653 self.expect_keyword_ident("from")?;
1654 }
1655 let mut grantees: Vec<String> = Vec::new();
1656 loop {
1657 // `GROUP name` is the legacy spelling of a plain role name.
1658 if self.peek_keyword_ident("group") {
1659 self.advance();
1660 }
1661 if self.peek_keyword_ident("public") {
1662 self.advance();
1663 grantees.push(String::new()); // PUBLIC
1664 } else {
1665 grantees.push(self.expect_ident_like()?);
1666 }
1667 if matches!(self.peek(), Token::Comma) {
1668 self.advance();
1669 } else {
1670 break;
1671 }
1672 }
1673 Ok(grantees)
1674 }
1675
1676 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1677 /// The body keeps its `$N` placeholders; substitution happens at
1678 /// EXECUTE. The declared types are recorded for
1679 /// `pg_prepared_statements.parameter_types` but are not enforced —
1680 /// PG infers when the list is omitted, and SPG resolves the values
1681 /// at substitution time either way.
1682 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1683 let start = self.pos;
1684 self.advance(); // PREPARE
1685 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1686 // different statement that happens to share the keyword. PG
1687 // ships with `max_prepared_transactions = 0` and reports it
1688 // this way; SPG has no prepared-transaction registry, so the
1689 // same wording is the accurate answer rather than a dodge.
1690 // Round 277 turned this from a silent no-op into a confusing
1691 // "expected AS in PREPARE" parse error.
1692 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1693 self.advance();
1694 let gid = match self.advance() {
1695 Token::String(g) => g,
1696 other => {
1697 return Err(self.err(alloc::format!(
1698 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1699 )));
1700 }
1701 };
1702 return Ok(Statement::PrepareTransaction(gid));
1703 }
1704 let name = self.expect_ident_like()?;
1705 let mut param_types = Vec::new();
1706 if matches!(self.peek(), Token::LParen) {
1707 self.advance();
1708 loop {
1709 let mut ty = self.expect_ident_like()?;
1710 // A parameterised type name (`numeric(10,2)`,
1711 // `varchar(20)`) keeps its argument list in the text.
1712 if matches!(self.peek(), Token::LParen) {
1713 let mut depth = 0usize;
1714 let mut buf = String::from("(");
1715 loop {
1716 match self.advance() {
1717 Token::LParen => {
1718 depth += 1;
1719 if depth > 1 {
1720 buf.push('(');
1721 }
1722 }
1723 Token::RParen => {
1724 depth -= 1;
1725 buf.push(')');
1726 if depth == 0 {
1727 break;
1728 }
1729 }
1730 Token::Comma => buf.push(','),
1731 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1732 Token::Eof => break,
1733 _ => {}
1734 }
1735 }
1736 ty.push_str(&buf);
1737 }
1738 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1739 // position, same family as the parameter list above.
1740 let array_suffix = self.consume_array_suffix();
1741 ty.push_str(&array_suffix);
1742 param_types.push(ty);
1743 match self.peek() {
1744 Token::Comma => {
1745 self.advance();
1746 }
1747 Token::RParen => {
1748 self.advance();
1749 break;
1750 }
1751 other => {
1752 return Err(self.err(alloc::format!(
1753 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1754 )));
1755 }
1756 }
1757 }
1758 }
1759 if !matches!(self.peek(), Token::As) {
1760 return Err(self.err(alloc::format!(
1761 "expected AS in PREPARE, got {:?}",
1762 self.peek()
1763 )));
1764 }
1765 self.advance();
1766 let body = self.parse_one_statement()?;
1767 // The Parser holds tokens, not the source text, so the
1768 // statement PG reports in `pg_prepared_statements.statement`
1769 // is rebuilt from the AST rather than sliced from the input.
1770 let _ = start;
1771 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1772 if !param_types.is_empty() {
1773 source.push_str(" (");
1774 source.push_str(¶m_types.join(", "));
1775 source.push(')');
1776 }
1777 source.push_str(" AS ");
1778 source.push_str(&alloc::format!("{body}"));
1779 Ok(Statement::Prepare {
1780 name,
1781 param_types,
1782 body: alloc::boxed::Box::new(body),
1783 source,
1784 })
1785 }
1786
1787 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1788 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1789 self.advance(); // EXECUTE
1790 let name = self.expect_ident_like()?;
1791 let mut args = Vec::new();
1792 if matches!(self.peek(), Token::LParen) {
1793 self.advance();
1794 if matches!(self.peek(), Token::RParen) {
1795 self.advance();
1796 } else {
1797 loop {
1798 args.push(self.parse_expr(0)?);
1799 match self.advance() {
1800 Token::Comma => {}
1801 Token::RParen => break,
1802 other => {
1803 return Err(self.err(alloc::format!(
1804 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1805 )));
1806 }
1807 }
1808 }
1809 }
1810 }
1811 Ok(Statement::Execute { name, args })
1812 }
1813
1814 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1815 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1816 /// procedure catalog yet, so this reports PG's not-found error
1817 /// (with its HINT) rather than pretending the call ran.
1818 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1819 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1820 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1821 self.advance(); // DISCARD
1822 let target = match self.advance() {
1823 Token::All => DiscardTarget::All,
1824 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1825 "all" => DiscardTarget::All,
1826 "plans" => DiscardTarget::Plans,
1827 "sequences" => DiscardTarget::Sequences,
1828 "temp" | "temporary" => DiscardTarget::Temp,
1829 other => {
1830 return Err(self.err(format!(
1831 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1832 )));
1833 }
1834 },
1835 other => {
1836 return Err(self.err(format!(
1837 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1838 )));
1839 }
1840 };
1841 Ok(Statement::Discard(target))
1842 }
1843
1844 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1845 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1846 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1847 /// aggressively the server interrupts, which SPG does not distinguish.
1848 /// Bare `KILL <id>` means CONNECTION.
1849 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1850 self.advance(); // KILL
1851 let mut query_only = false;
1852 loop {
1853 // CONNECTION is a reserved keyword token (it also opens
1854 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1855 // `Token::Connection` rather than a bare ident.
1856 if matches!(self.peek(), Token::Connection) {
1857 self.advance();
1858 break;
1859 }
1860 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1861 break;
1862 };
1863 match w.to_ascii_lowercase().as_str() {
1864 "hard" | "soft" => {
1865 self.advance();
1866 }
1867 "query" => {
1868 self.advance();
1869 query_only = true;
1870 break;
1871 }
1872 _ => break,
1873 }
1874 }
1875 let id = self.parse_expr(0)?;
1876 Ok(Statement::Kill {
1877 query_only,
1878 id: Box::new(id),
1879 })
1880 }
1881
1882 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1883 self.advance(); // CALL
1884 let name = self.expect_ident_like()?;
1885 self.consume_until_statement_boundary();
1886 Ok(Statement::Call(name))
1887 }
1888
1889 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1890 self.advance(); // DEALLOCATE
1891 // PG accepts an optional noise `PREPARE` keyword here.
1892 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1893 self.advance();
1894 }
1895 if matches!(self.peek(), Token::All) {
1896 self.advance();
1897 return Ok(Statement::Deallocate(None));
1898 }
1899 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1900 self.advance();
1901 return Ok(Statement::Deallocate(None));
1902 }
1903 let name = self.expect_ident_like()?;
1904 Ok(Statement::Deallocate(Some(name)))
1905 }
1906
1907 fn consume_until_statement_boundary(&mut self) {
1908 loop {
1909 match self.peek() {
1910 Token::Semicolon | Token::Eof => return,
1911 _ => self.advance(),
1912 };
1913 }
1914 }
1915
1916 /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1917 /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1918 /// runs after it and eats the rest.
1919 fn scan_database_name(&mut self) -> Option<String> {
1920 // The caller has only PEEKED at `DATABASE`; step past it, or the
1921 // first identifier found is the keyword itself. It was, and
1922 // `pg_database` listed a database called `database`.
1923 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1924 {
1925 self.advance();
1926 }
1927 for kw in ["if", "not", "exists"] {
1928 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1929 {
1930 self.advance();
1931 }
1932 }
1933 match self.peek().clone() {
1934 Token::Ident(w) | Token::QuotedIdent(w) => {
1935 self.advance();
1936 Some(w)
1937 }
1938 _ => None,
1939 }
1940 }
1941
1942 /// v7.38.18 — consume to the statement boundary like
1943 /// `consume_until_statement_boundary`, but pick out the collation a
1944 /// `CREATE DATABASE` asked for on the way.
1945 ///
1946 /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1947 /// `LC_CTYPE` does not, because SPG has no separate ctype and
1948 /// pretending to honour it would be the more misleading answer. An
1949 /// `=` between the keyword and the value is optional, as in PG.
1950 ///
1951 /// The whole statement used to be thrown away. Being single-database
1952 /// makes the NAME a no-op; it does not make the collation one.
1953 fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1954 let mut want_value = false;
1955 let mut found: Option<String> = None;
1956 loop {
1957 let tok = self.peek().clone();
1958 match &tok {
1959 Token::Semicolon | Token::Eof => break,
1960 Token::Ident(w) | Token::QuotedIdent(w)
1961 if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1962 {
1963 want_value = true;
1964 }
1965 Token::Eq if want_value => {}
1966 Token::String(v) if want_value => {
1967 found = Some(v.clone());
1968 want_value = false;
1969 }
1970 Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1971 found = Some(v.clone());
1972 want_value = false;
1973 }
1974 _ => want_value = false,
1975 }
1976 self.advance();
1977 }
1978 found
1979 }
1980
1981 /// v7.22 (round-13 T2) — consume to the statement boundary like
1982 /// `consume_until_statement_boundary`, but pick out the sequence
1983 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1984 /// columns) or the first string literal (`nextval('<seq>')`).
1985 /// Schema qualifiers and `::regclass` casts are stripped.
1986 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1987 let mut seq: Option<String> = None;
1988 let mut after_sequence_kw = false;
1989 let mut after_name_kw = false;
1990 loop {
1991 match self.peek().clone() {
1992 Token::Semicolon | Token::Eof => break,
1993 Token::Ident(s) | Token::QuotedIdent(s) => {
1994 if after_name_kw && seq.is_none() {
1995 self.advance();
1996 let mut name = s;
1997 // `SEQUENCE NAME public.groups_id_seq` — keep
1998 // the bare name, drop qualifiers.
1999 while matches!(self.peek(), Token::Dot) {
2000 self.advance();
2001 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
2002 name = n;
2003 }
2004 }
2005 seq = Some(name);
2006 after_name_kw = false;
2007 continue;
2008 }
2009 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
2010 after_name_kw = true;
2011 after_sequence_kw = false;
2012 } else {
2013 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
2014 }
2015 self.advance();
2016 }
2017 Token::String(s) => {
2018 if seq.is_none() {
2019 // `nextval('public.groups_id_seq'::regclass)`
2020 let bare = s
2021 .rsplit_once('.')
2022 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
2023 seq = Some(bare);
2024 }
2025 self.advance();
2026 }
2027 _ => {
2028 after_sequence_kw = false;
2029 after_name_kw = false;
2030 self.advance();
2031 }
2032 }
2033 }
2034 seq
2035 }
2036
2037 /// v7.39 (round 621) — is the next token the keyword `BY`?
2038 ///
2039 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2040 /// column, table and alias name — and SPG lexed it into a dedicated
2041 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2042 /// two-letter keywords the lexer knew, this was the only one PG leaves
2043 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2044 ///
2045 /// The token is gone; the three clauses that own the word — GROUP BY,
2046 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2047 /// ask this instead. Adding it to the unreserved-identifier table was not
2048 /// enough on its own: identifier positions that match the token shape
2049 /// directly (an index's column list, a table alias) never consult that
2050 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2051 /// Not lexing it as a keyword closes the whole class rather than the two
2052 /// positions that happened to be noticed.
2053 fn peek_is_by(&self) -> bool {
2054 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2055 }
2056
2057 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2058 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2059 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2060 fn consume_drop_behaviour(&mut self) {
2061 if matches!(
2062 self.peek(),
2063 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2064 ) {
2065 self.advance();
2066 }
2067 }
2068
2069 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2070 let first = match self.advance() {
2071 Token::Ident(s) | Token::QuotedIdent(s) => s,
2072 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2073 // per PG's `pg_get_keywords()` classification. SPG tokenizes
2074 // these as named variants for parsing leverage in the
2075 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2076 // `BEGIN`, etc.), but they MUST still be usable as table /
2077 // column / alias names in DDL+DML. Sentori migrations like
2078 // 0001_init.sql ship `release TEXT NOT NULL` in the events
2079 // table — the `events.release` column carries the release
2080 // identifier string. Pre-T4 this triggered "expected
2081 // identifier, got Release" and blocked every drop-in user
2082 // whose schema had a column / alias with one of these names.
2083 other if unreserved_keyword_text(&other).is_some() => {
2084 unreserved_keyword_text(&other).unwrap()
2085 }
2086 other => {
2087 return Err(ParseError {
2088 message: format!("expected identifier, got {other:?}"),
2089 token_pos: self.consumed_pos(),
2090 });
2091 }
2092 };
2093 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2094 // qualify every name with `public.` (and pg_catalog.* for
2095 // functions); SPG is single-schema so we discard the
2096 // prefix and return only the trailing ident. Same shape
2097 // also handles MySQL `db.tbl` cross-database refs (SPG
2098 // ignores the db part).
2099 if matches!(self.peek(), Token::Dot) {
2100 self.advance();
2101 match self.advance() {
2102 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2103 other if unreserved_keyword_text(&other).is_some() => {
2104 return Ok(unreserved_keyword_text(&other).unwrap());
2105 }
2106 other => {
2107 return Err(ParseError {
2108 message: format!("expected identifier after '{first}.', got {other:?}"),
2109 token_pos: self.consumed_pos(),
2110 });
2111 }
2112 }
2113 }
2114 Ok(first)
2115 }
2116
2117 #[allow(clippy::too_many_lines)]
2118 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2119 // v7.14.0 — empty / comment-only / semicolon-only input
2120 // (after the lexer strips line + block + MySQL
2121 // conditional comments) lands as Statement::Empty.
2122 // pg_dump and mysqldump emit several wrappers that
2123 // collapse to nothing after stripping (`/*!40101 SET …
2124 // */;`, blank lines between statements); the engine
2125 // returns CommandOk no-op so the dump loads cleanly.
2126 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2127 return Ok(Statement::Empty);
2128 }
2129 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2130 // catalog / metadata DDL that has no behavioural effect
2131 // on SPG's single-schema, single-database, single-user
2132 // model. Consume the whole statement up to the next
2133 // semicolon / EOF and return Empty. This is broader than
2134 // the per-keyword DROP / SET / COMMENT arms but lets the
2135 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2136 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2137 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2138 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2139 let lc = s.to_ascii_lowercase();
2140 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2141 if lc == "comment" {
2142 return self.parse_comment_on();
2143 }
2144 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2145 if lc == "grant" || lc == "revoke" {
2146 return self.parse_grant_or_revoke(lc == "grant");
2147 }
2148 // v7.39 (round 277) — the SQL-level prepared-statement
2149 // surface is REAL now. It used to be accepted and dropped
2150 // on the theory that "real execution still happens via the
2151 // extended-query flow" — true only for a driver that uses
2152 // that flow; a plain SQL PREPARE / EXECUTE returned no
2153 // rows at all.
2154 if lc == "prepare" {
2155 return self.parse_prepare();
2156 }
2157 if lc == "execute" {
2158 return self.parse_execute();
2159 }
2160 if lc == "deallocate" {
2161 return self.parse_deallocate();
2162 }
2163 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2164 // accepted and dropped, so an application's stored-procedure
2165 // invocation reported success and did nothing. SPG has no
2166 // procedure catalog, so every CALL names a procedure that
2167 // does not exist — which is exactly what PG says.
2168 if lc == "call" {
2169 return self.parse_call();
2170 }
2171 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2172 // names one connection and acts on it.
2173 if lc == "kill" {
2174 return self.parse_kill();
2175 }
2176 if lc == "discard" {
2177 return self.parse_discard();
2178 }
2179 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2180 // Still performs nothing; the roles are carried out so a name
2181 // that does not exist is refused, as PG18 refuses it.
2182 if lc == "reassign" {
2183 self.advance();
2184 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2185 self.advance();
2186 }
2187 if self.peek_is_by() {
2188 self.advance();
2189 }
2190 // Only the roles BEFORE the TO are the ones that must
2191 // exist — `TO` names the new owner, which PG checks as
2192 // well, so both lists are collected.
2193 let mut names = self.take_comma_separated_names();
2194 if matches!(self.peek(), Token::To) {
2195 self.advance();
2196 names.extend(self.take_comma_separated_names());
2197 }
2198 self.consume_until_statement_boundary();
2199 return Ok(Statement::ValidateOnly {
2200 kind: crate::ast::ValidateOnlyKind::RoleName,
2201 names,
2202 });
2203 }
2204 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2205 // unconditionally with `no security label providers have been
2206 // loaded`, whatever object it names, because none is loaded.
2207 // SPG has none either; accepting it told the caller a label had
2208 // been applied when nothing anywhere records one.
2209 if lc == "security" {
2210 self.consume_until_statement_boundary();
2211 return Ok(Statement::ValidateOnly {
2212 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2213 names: Vec::new(),
2214 });
2215 }
2216 // v7.39.2 — `USE <db>` is a real statement now, and only in
2217 // the MySQL dialect. It used to be swallowed here with the
2218 // dump noise, so `USE myapp; SELECT DATABASE()` answered the
2219 // same constant it answered before — MySQL 9.7.2 answers
2220 // `myapp`. PostgreSQL has no USE at all, and pg_dump does not
2221 // emit one, but the swallow stays on that side: it was put
2222 // there for restores and taking it away is not this defect.
2223 if lc == "use" {
2224 if self.mysql_dialect {
2225 self.advance();
2226 let name = self.expect_ident_like()?;
2227 return Ok(Statement::UseDatabase(name));
2228 }
2229 self.consume_until_statement_boundary();
2230 return Ok(Statement::Empty);
2231 }
2232 if is_dump_noise_statement(&lc) {
2233 self.consume_until_statement_boundary();
2234 return Ok(Statement::Empty);
2235 }
2236 }
2237 match self.peek() {
2238 Token::Select => self.parse_select_stmt(),
2239 // v7.37.17 (17.6 siblings) — a statement opening with a
2240 // parenthesized query group: `(SELECT … UNION …)
2241 // INTERSECT …`. parse_bare_select's group arm consumes
2242 // the parens; the select parser handles the outer chain
2243 // and tail.
2244 Token::LParen
2245 if matches!(
2246 self.tokens.get(self.pos + 1),
2247 Some(Token::Select | Token::LParen | Token::Values)
2248 ) =>
2249 {
2250 self.parse_select_stmt()
2251 }
2252 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2253 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2254 // Lowers to the same UNION ALL chain the FROM-position
2255 // form uses, then reuses the shared SELECT tail.
2256 Token::Values => {
2257 self.advance(); // VALUES
2258 let mut head = self.parse_values_rows_body()?;
2259 // v7.40.11 — and it may HEAD a set-operation chain:
2260 // `VALUES (1) UNION ALL SELECT 2`. The CTE-body form has
2261 // done this since the recursive-seed work; the top-level
2262 // statement went straight to the tail and reported
2263 // `syntax error at or near "UNION"`.
2264 self.parse_setop_chain_into(&mut head)?;
2265 self.parse_select_tail_into(&mut head)?;
2266 Ok(Statement::Select(head))
2267 }
2268 // SQL-standard `TABLE name` shorthand for
2269 // `SELECT * FROM name` — pg_dump never emits it, but
2270 // psql users and PG docs use it constantly. Set-op
2271 // chains and the ORDER BY/LIMIT tail compose like any
2272 // SELECT head.
2273 Token::Table
2274 if matches!(
2275 self.tokens.get(self.pos + 1),
2276 Some(Token::Ident(_) | Token::QuotedIdent(_))
2277 ) =>
2278 {
2279 let mut head = self.parse_table_shorthand()?;
2280 self.parse_setop_chain_into(&mut head)?;
2281 self.parse_select_tail_into(&mut head)?;
2282 Ok(Statement::Select(head))
2283 }
2284 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2285 // body is a dollar-quoted plpgsql block (lexer already
2286 // collapsed `$$…$$` into a single Token::String).
2287 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2288 // real PlPgSqlBlock so the engine can EXECUTE it at
2289 // top level instead of silently swallowing. Pre-
2290 // v7.16.2 the parser threw the body away and the
2291 // engine returned CommandOk for the entire DO; that
2292 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2293 // $$` into a SEV-1 silent no-op (the IF + the rename
2294 // were both invisible — mailrs's migrate-042 didn't
2295 // actually run). Now the body parses + executes;
2296 // EmbeddedSql inside the block runs immediately
2297 // against the engine (not deferred — we're at top
2298 // level, not inside a trigger row-write loop).
2299 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2300 self.advance();
2301 let body_text = match self.advance() {
2302 Token::String(s) => s,
2303 other => {
2304 return Err(self.err(alloc::format!(
2305 "expected dollar-quoted body after DO, got {other:?}"
2306 )));
2307 }
2308 };
2309 // Optional `LANGUAGE <name>` trailer (idents only).
2310 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2311 self.advance();
2312 let _ = self.expect_ident_like()?;
2313 }
2314 // Parse the body — same shape CREATE FUNCTION
2315 // uses for trigger function bodies. If the body
2316 // doesn't parse cleanly we surface the error
2317 // (better than silent no-op).
2318 let block = parse_plpgsql_body(&body_text)?;
2319 Ok(Statement::DoBlock(block))
2320 }
2321 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2322 // WITH isn't a reserved token in our lexer — comes through
2323 // as `Token::Ident("with")` (case-insensitive).
2324 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2325 self.advance();
2326 self.parse_with_cte_then_select()
2327 }
2328 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2329 // an identifier — not a reserved keyword.
2330 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2331 self.advance();
2332 let mut analyze = false;
2333 let mut suggest = false;
2334 let mut costs_off = false;
2335 let mut buffers = false;
2336 let mut timing_off = false;
2337 let mut settings = false;
2338 let mut wal = false;
2339 let mut summary_off = false;
2340 let mut format = crate::ast::ExplainFormat::Text;
2341 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2342 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2343 // options are comma-separated. Booleans default to ON
2344 // when the value token is omitted (matches PG).
2345 if matches!(self.peek(), Token::LParen) {
2346 self.advance();
2347 loop {
2348 let opt = match self.peek().clone() {
2349 Token::Ident(s) | Token::QuotedIdent(s) => s,
2350 other => {
2351 return Err(self.err(format!(
2352 "expected option keyword inside EXPLAIN (…), got {other:?}"
2353 )));
2354 }
2355 };
2356 self.advance();
2357 if opt.eq_ignore_ascii_case("suggest") {
2358 suggest = true;
2359 // SUGGEST takes no explicit value today.
2360 } else if opt.eq_ignore_ascii_case("costs") {
2361 // PG syntax: `COSTS [ON | OFF]`. Default
2362 // when value omitted is ON, so plain
2363 // `COSTS` is a no-op. `COSTS OFF` flips.
2364 // `ON` lexes to `Token::On` (reserved
2365 // keyword in JOIN ... ON contexts); accept
2366 // it alongside the bare Ident form so the
2367 // grammar matches PG verbatim.
2368 let value = match self.peek().clone() {
2369 Token::On => {
2370 self.advance();
2371 true
2372 }
2373 Token::Ident(v) | Token::QuotedIdent(v)
2374 if v.eq_ignore_ascii_case("off") =>
2375 {
2376 self.advance();
2377 false
2378 }
2379 Token::Ident(v) | Token::QuotedIdent(v)
2380 if v.eq_ignore_ascii_case("true") =>
2381 {
2382 self.advance();
2383 true
2384 }
2385 _ => true,
2386 };
2387 costs_off = !value;
2388 } else if opt.eq_ignore_ascii_case("analyze")
2389 || opt.eq_ignore_ascii_case("analyse")
2390 {
2391 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2392 // Same default-ON rule as ANALYZE keyword form.
2393 let value = match self.peek().clone() {
2394 Token::On => {
2395 self.advance();
2396 true
2397 }
2398 Token::Ident(v) | Token::QuotedIdent(v)
2399 if v.eq_ignore_ascii_case("off") =>
2400 {
2401 self.advance();
2402 false
2403 }
2404 Token::Ident(v) | Token::QuotedIdent(v)
2405 if v.eq_ignore_ascii_case("true") =>
2406 {
2407 self.advance();
2408 true
2409 }
2410 _ => true,
2411 };
2412 analyze = value;
2413 } else if opt.eq_ignore_ascii_case("buffers") {
2414 // v7.37.22 — `BUFFERS [ON|OFF]`.
2415 let value = match self.peek().clone() {
2416 Token::On => {
2417 self.advance();
2418 true
2419 }
2420 Token::Ident(v) | Token::QuotedIdent(v)
2421 if v.eq_ignore_ascii_case("off") =>
2422 {
2423 self.advance();
2424 false
2425 }
2426 Token::Ident(v) | Token::QuotedIdent(v)
2427 if v.eq_ignore_ascii_case("true") =>
2428 {
2429 self.advance();
2430 true
2431 }
2432 _ => true,
2433 };
2434 buffers = value;
2435 } else if opt.eq_ignore_ascii_case("timing") {
2436 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2437 // the measured wall-clock annotation.
2438 let value = match self.peek().clone() {
2439 Token::On => {
2440 self.advance();
2441 true
2442 }
2443 Token::Ident(v) | Token::QuotedIdent(v)
2444 if v.eq_ignore_ascii_case("off") =>
2445 {
2446 self.advance();
2447 false
2448 }
2449 Token::Ident(v) | Token::QuotedIdent(v)
2450 if v.eq_ignore_ascii_case("true") =>
2451 {
2452 self.advance();
2453 true
2454 }
2455 _ => true,
2456 };
2457 timing_off = !value;
2458 } else if opt.eq_ignore_ascii_case("settings") {
2459 settings = true;
2460 } else if opt.eq_ignore_ascii_case("wal") {
2461 wal = true;
2462 } else if opt.eq_ignore_ascii_case("summary") {
2463 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2464 // gates the trailing Planning/Execution Time
2465 // lines now (was accept-and-no-op).
2466 let value = match self.peek().clone() {
2467 Token::On => {
2468 self.advance();
2469 true
2470 }
2471 Token::Ident(v) | Token::QuotedIdent(v)
2472 if v.eq_ignore_ascii_case("off") =>
2473 {
2474 self.advance();
2475 false
2476 }
2477 Token::Ident(v) | Token::QuotedIdent(v)
2478 if v.eq_ignore_ascii_case("true") =>
2479 {
2480 self.advance();
2481 true
2482 }
2483 _ => true,
2484 };
2485 summary_off = !value;
2486 } else if opt.eq_ignore_ascii_case("verbose")
2487 || opt.eq_ignore_ascii_case("format")
2488 {
2489 // v7.37.22 — accept-but-no-op the remaining
2490 // PG options so EXPLAIN-using clients
2491 // (pgAdmin / DataGrip) don't see syntax
2492 // errors. FORMAT takes a value (text /
2493 // json / yaml / xml); skip the next token
2494 // if it's an ident.
2495 if opt.eq_ignore_ascii_case("format") {
2496 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2497 {
2498 self.advance();
2499 format = match v.to_ascii_lowercase().as_str() {
2500 "text" => crate::ast::ExplainFormat::Text,
2501 "json" => crate::ast::ExplainFormat::Json,
2502 "xml" => crate::ast::ExplainFormat::Xml,
2503 "yaml" => crate::ast::ExplainFormat::Yaml,
2504 other => {
2505 return Err(self.err(format!(
2506 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2507 supports text, json, xml, yaml"
2508 )));
2509 }
2510 };
2511 }
2512 } else {
2513 // VERBOSE / SUMMARY take optional ON/OFF;
2514 // consume if present.
2515 if matches!(self.peek(), Token::On) {
2516 self.advance();
2517 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2518 self.peek().clone()
2519 && (v.eq_ignore_ascii_case("off")
2520 || v.eq_ignore_ascii_case("true"))
2521 {
2522 self.advance();
2523 let _ = v;
2524 }
2525 }
2526 } else {
2527 return Err(self.err(format!(
2528 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2529 )));
2530 }
2531 if matches!(self.peek(), Token::Comma) {
2532 self.advance();
2533 continue;
2534 }
2535 break;
2536 }
2537 if !matches!(self.peek(), Token::RParen) {
2538 return Err(self.err(format!(
2539 "expected ')' after EXPLAIN options, got {:?}",
2540 self.peek()
2541 )));
2542 }
2543 self.advance();
2544 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2545 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2546 {
2547 self.advance();
2548 analyze = true;
2549 }
2550 // v7.39 (round 224) — the body may open with WITH (CTEs);
2551 // route through the same CTE-then-SELECT path the top-level
2552 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2553 // too (PG explains INSERT / UPDATE / DELETE).
2554 let inner = match self.peek().clone() {
2555 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2556 self.advance();
2557 self.parse_with_cte_then_select()?
2558 }
2559 Token::Insert => self.parse_insert_stmt(false)?,
2560 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2561 self.advance();
2562 self.parse_update_after_keyword()?
2563 }
2564 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2565 self.advance();
2566 self.parse_delete_after_keyword()?
2567 }
2568 _ => self.parse_select_stmt()?,
2569 };
2570 if !matches!(
2571 inner,
2572 Statement::Select(_)
2573 | Statement::Insert(_)
2574 | Statement::Update(_)
2575 | Statement::Delete(_)
2576 ) {
2577 return Err(self.err(format!(
2578 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2579 )));
2580 }
2581 Ok(Statement::Explain(crate::ast::ExplainStatement {
2582 analyze,
2583 inner: Box::new(inner),
2584 suggest,
2585 costs_off,
2586 buffers,
2587 timing_off,
2588 settings,
2589 wal,
2590 summary_off,
2591 format,
2592 }))
2593 }
2594 Token::Create => self.parse_create_stmt(),
2595 Token::Insert => self.parse_insert_stmt(false),
2596 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2597 // spelling; route to the same handler. DESC is the
2598 // reserved ORDER BY token, so it gets its own arm.
2599 Token::Ident(s)
2600 if s.eq_ignore_ascii_case("describe")
2601 && matches!(
2602 self.tokens.get(self.pos + 1),
2603 Some(Token::Ident(_) | Token::QuotedIdent(_))
2604 ) =>
2605 {
2606 self.advance();
2607 let table = self.expect_ident_like()?;
2608 Ok(Statement::ShowColumns(table))
2609 }
2610 Token::Desc
2611 if matches!(
2612 self.tokens.get(self.pos + 1),
2613 Some(Token::Ident(_) | Token::QuotedIdent(_))
2614 ) =>
2615 {
2616 self.advance();
2617 let table = self.expect_ident_like()?;
2618 Ok(Statement::ShowColumns(table))
2619 }
2620 // `COPY table [(cols)] TO STDOUT` — the export half of
2621 // pg_dump's COPY pair (the FROM stdin half rides the
2622 // embed import path). Options need a format design and
2623 // error honestly.
2624 Token::Ident(s)
2625 if s.eq_ignore_ascii_case("copy")
2626 && matches!(
2627 self.tokens.get(self.pos + 1),
2628 Some(Token::Ident(_) | Token::QuotedIdent(_))
2629 ) =>
2630 {
2631 self.advance(); // COPY
2632 let table = self.expect_ident_like()?;
2633 let columns = if matches!(self.peek(), Token::LParen) {
2634 self.advance();
2635 let mut cols = alloc::vec![self.expect_ident_like()?];
2636 while matches!(self.peek(), Token::Comma) {
2637 self.advance();
2638 cols.push(self.expect_ident_like()?);
2639 }
2640 if !matches!(self.peek(), Token::RParen) {
2641 return Err(self.err(format!(
2642 "expected ')' after COPY column list, got {:?}",
2643 self.peek()
2644 )));
2645 }
2646 self.advance();
2647 Some(cols)
2648 } else {
2649 None
2650 };
2651 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2652 // endpoint. (FROM STDIN still rides the wire/import path —
2653 // its data arrives out of band.)
2654 if matches!(self.peek(), Token::From)
2655 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2656 {
2657 self.advance(); // FROM
2658 let Token::String(path) = self.advance() else {
2659 unreachable!()
2660 };
2661 let options = self.parse_copy_to_options()?;
2662 return Ok(Statement::CopyFromFile {
2663 table,
2664 columns,
2665 path,
2666 options,
2667 });
2668 }
2669 if !matches!(self.peek(), Token::To) {
2670 return Err(self.err(format!(
2671 "COPY: only TO STDOUT is supported here (FROM stdin \
2672 rides the import path); got {:?}",
2673 self.peek()
2674 )));
2675 }
2676 self.advance();
2677 if matches!(self.peek(), Token::String(_)) {
2678 let Token::String(path) = self.advance() else { unreachable!() };
2679 let options = self.parse_copy_to_options()?;
2680 return Ok(Statement::CopyToFile {
2681 table,
2682 columns,
2683 query: None,
2684 path,
2685 options,
2686 });
2687 }
2688 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2689 return Err(self.err(format!(
2690 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2691 self.peek()
2692 )));
2693 }
2694 self.advance();
2695 let options = self.parse_copy_to_options()?;
2696 Ok(Statement::CopyTo {
2697 table,
2698 columns,
2699 query: None,
2700 options,
2701 })
2702 }
2703 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2704 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2705 // result set is streamed in COPY format (PG's query form).
2706 Token::Ident(s)
2707 if s.eq_ignore_ascii_case("copy")
2708 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2709 {
2710 self.advance(); // COPY
2711 self.advance(); // (
2712 let query = self.parse_select_stmt()?;
2713 if !matches!(self.peek(), Token::RParen) {
2714 return Err(self.err(format!(
2715 "expected ')' after COPY query, got {:?}",
2716 self.peek()
2717 )));
2718 }
2719 self.advance(); // )
2720 if !matches!(self.peek(), Token::To) {
2721 return Err(self.err(format!(
2722 "COPY (query): only TO STDOUT is supported, got {:?}",
2723 self.peek()
2724 )));
2725 }
2726 self.advance();
2727 if matches!(self.peek(), Token::String(_)) {
2728 let Token::String(path) = self.advance() else { unreachable!() };
2729 let options = self.parse_copy_to_options()?;
2730 return Ok(Statement::CopyToFile {
2731 table: String::new(),
2732 columns: None,
2733 query: Some(alloc::boxed::Box::new(query)),
2734 path,
2735 options,
2736 });
2737 }
2738 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2739 return Err(self.err(format!(
2740 "COPY (query): TO supports STDOUT only, got {:?}",
2741 self.peek()
2742 )));
2743 }
2744 self.advance();
2745 let options = self.parse_copy_to_options()?;
2746 Ok(Statement::CopyTo {
2747 table: String::new(),
2748 columns: None,
2749 query: Some(alloc::boxed::Box::new(query)),
2750 options,
2751 })
2752 }
2753 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2754 // Shares the INSERT body; the replace flag lowers it
2755 // onto ON CONFLICT DO UPDATE with an empty assignment
2756 // list (engine: replace the whole row).
2757 Token::Ident(s)
2758 if s.eq_ignore_ascii_case("replace")
2759 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2760 {
2761 self.parse_insert_stmt(true)
2762 }
2763 Token::Begin => {
2764 self.advance();
2765 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2766 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2767 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2768 // is consumed first, then the trailing modes — including the
2769 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2770 // WORK/TRANSACTION). The explicit level, when present, rides the
2771 // statement so `exec_begin` applies it for this transaction.
2772 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2773 {
2774 self.advance();
2775 }
2776 let iso = self.parse_isolation_level_clauses()?;
2777 Ok(Statement::Begin(iso))
2778 }
2779 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2780 // for BEGIN. START is contextual in PG too; pattern-match
2781 // on the ident here. Iso clauses are parse-and-ignored,
2782 // same as BEGIN above.
2783 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2784 self.advance();
2785 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2786 {
2787 return Err(self.err(alloc::format!(
2788 "expected TRANSACTION after START, got {:?}",
2789 self.peek()
2790 )));
2791 }
2792 self.advance();
2793 let iso = self.parse_isolation_level_clauses()?;
2794 Ok(Statement::Begin(iso))
2795 }
2796 Token::Commit => {
2797 self.advance();
2798 // PG: `COMMIT [WORK | TRANSACTION]`.
2799 if let Token::Ident(w) = self.peek()
2800 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2801 {
2802 self.advance();
2803 }
2804 Ok(Statement::Commit)
2805 }
2806 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2807 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2808 // every transaction with `END;` and the drop-in aborted on
2809 // it. Only reachable at statement start (CASE … END lives
2810 // inside expressions), so no ambiguity.
2811 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2812 self.advance();
2813 if let Token::Ident(w) = self.peek()
2814 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2815 {
2816 self.advance();
2817 }
2818 Ok(Statement::Commit)
2819 }
2820 Token::Rollback => {
2821 self.advance();
2822 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2823 // savepoint without ending the transaction. Bare
2824 // `ROLLBACK` drops the whole TX.
2825 if matches!(self.peek(), Token::To) {
2826 self.advance();
2827 if matches!(self.peek(), Token::Savepoint) {
2828 self.advance();
2829 }
2830 let name = self.expect_ident_like()?;
2831 Ok(Statement::RollbackToSavepoint(name))
2832 } else {
2833 Ok(Statement::Rollback)
2834 }
2835 }
2836 Token::Savepoint => {
2837 self.advance();
2838 let name = self.expect_ident_like()?;
2839 Ok(Statement::Savepoint(name))
2840 }
2841 Token::Release => {
2842 self.advance();
2843 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2844 // is optional in standard SQL.
2845 if matches!(self.peek(), Token::Savepoint) {
2846 self.advance();
2847 }
2848 let name = self.expect_ident_like()?;
2849 Ok(Statement::ReleaseSavepoint(name))
2850 }
2851 Token::Show => {
2852 self.advance();
2853 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2854 // v6.1.2 promoted TABLES to a reserved keyword (for
2855 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2856 // arrives as `Token::Tables` rather than a bare ident.
2857 // USERS / COLUMNS remain bare idents.
2858 let target = match self.advance() {
2859 Token::Tables => "tables".to_string(),
2860 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2861 // keyword token; recognise it as the SHOW CREATE
2862 // dispatch keyword too.
2863 Token::Create => "create".to_string(),
2864 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2865 // keyword too; let SHOW INDEX FROM parse.
2866 Token::Index => "index".to_string(),
2867 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2868 // reserved (used in aggregate function calls);
2869 // recognise it here so the parser dispatches
2870 // to ShowParameter("all") — the engine returns
2871 // the curated parameter inventory.
2872 Token::All => "all".to_string(),
2873 // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2874 // spelling for the size of the diagnostics area.
2875 // MySQL-dialect only: PostgreSQL 18.4 answers this
2876 // phrase with `syntax error at or near "("`, and a
2877 // PG session must keep getting exactly that rather
2878 // than a message about an unknown parameter.
2879 // `COUNT` arrives as a bare ident; the `(*)` and the
2880 // trailing keyword are consumed here so the whole
2881 // form reaches the engine as one parameter name.
2882 Token::Ident(ref c)
2883 if self.mysql_dialect
2884 && c.eq_ignore_ascii_case("count")
2885 && matches!(self.peek(), Token::LParen) =>
2886 {
2887 self.advance();
2888 if matches!(self.peek(), Token::Star) {
2889 self.advance();
2890 }
2891 if matches!(self.peek(), Token::RParen) {
2892 self.advance();
2893 }
2894 match self.advance() {
2895 Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2896 return Ok(Statement::ShowParameter(
2897 "count(*) warnings".to_string(),
2898 ));
2899 }
2900 other => {
2901 return Err(self.err(format!(
2902 "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2903 )));
2904 }
2905 }
2906 }
2907 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2908 other => {
2909 return Err(self.err(format!(
2910 "expected SHOW target, got {other:?}"
2911 )));
2912 }
2913 };
2914 match target.as_str() {
2915 "tables" => Ok(Statement::ShowTables),
2916 "users" => Ok(Statement::ShowUsers),
2917 // v7.38 轴 4 — `SHOW transaction_isolation`
2918 // returns the currently-selected isolation level.
2919 "transaction_isolation" => Ok(Statement::ShowParameter(
2920 "transaction_isolation".to_string(),
2921 )),
2922 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2923 // TABLE <t>` returns a 2-column row: (Table,
2924 // Create Table). mysqldump emits this for every
2925 // table at scrape time; without it the dump
2926 // round-trip stalls.
2927 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2928 // FROM <t>` (also spelled `SHOW INDEX` and
2929 // `SHOW KEYS`). admin / mysqldump probes use
2930 // it to list per-table indexes.
2931 "indexes" | "index" | "keys" => {
2932 if !matches!(self.peek(), Token::From) {
2933 return Err(self.err(format!(
2934 "expected FROM after SHOW INDEXES, got {:?}",
2935 self.peek()
2936 )));
2937 }
2938 self.advance();
2939 let table = self.expect_ident_like()?;
2940 Ok(Statement::ShowIndexes(table))
2941 }
2942 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2943 // `SHOW VARIABLES`. Both return a 2-column row
2944 // set listing server-side state; clients probe
2945 // them at connect time.
2946 "status" => Ok(Statement::ShowStatus),
2947 "variables" => {
2948 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2949 if matches!(self.peek(), Token::Like) {
2950 self.advance();
2951 let pat = match self.advance() {
2952 Token::String(p) => p,
2953 other => {
2954 return Err(self.err(format!(
2955 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2956 )));
2957 }
2958 };
2959 return Ok(Statement::ShowVariablesLike(pat));
2960 }
2961 Ok(Statement::ShowVariables)
2962 }
2963 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2964 "processlist" => Ok(Statement::ShowProcesslist),
2965 "create" => {
2966 // SHOW CREATE TABLE / VIEW / DATABASE — only
2967 // TABLE is supported in v7.17.
2968 let kind = match self.advance() {
2969 Token::Ident(s) | Token::QuotedIdent(s) => s,
2970 Token::Table => "table".to_string(),
2971 other => {
2972 return Err(self.err(format!(
2973 "expected TABLE after SHOW CREATE, got {other:?}"
2974 )));
2975 }
2976 };
2977 if !kind.eq_ignore_ascii_case("table") {
2978 return Err(self.err(format!(
2979 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2980 )));
2981 }
2982 let name = self.expect_ident_like()?;
2983 Ok(Statement::ShowCreateTable(name))
2984 }
2985 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2986 // (and `SHOW SCHEMAS` alias). The mysql client uses
2987 // it to populate the database selector at connect
2988 // time; without it `mysql -p` errors before the
2989 // first user query.
2990 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2991 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2992 // keyword on its own; it lands here as a bare
2993 // ident. Returning all publications + their
2994 // scope summary.
2995 "publications" => Ok(Statement::ShowPublications),
2996 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2997 "subscriptions" => Ok(Statement::ShowSubscriptions),
2998 "columns" => {
2999 if !matches!(self.peek(), Token::From) {
3000 return Err(self.err(format!(
3001 "expected FROM after SHOW COLUMNS, got {:?}",
3002 self.peek()
3003 )));
3004 }
3005 self.advance();
3006 let table = self.expect_ident_like()?;
3007 Ok(Statement::ShowColumns(table))
3008 }
3009 // v7.38 轴 4 surface — `SHOW <param>` for any
3010 // remaining session / preset parameter name
3011 // (server_version, search_path, client_encoding,
3012 // …). The engine's ShowParameter handler does the
3013 // dispatch; unrecognised names error there with
3014 // a pointer to pg_settings, not at parse time —
3015 // so a driver that issues `SHOW spam_setting`
3016 // gets a clear runtime error instead of a
3017 // confusing "unknown SHOW target".
3018 // v7.40.11 — PG's own spelling of the isolation
3019 // probe, which is what every driver sends and what
3020 // its own documentation writes. `transaction` is a
3021 // bare ident here, so the target matched and the two
3022 // words after it did not: the statement ended at
3023 // `transaction` and the parser reported
3024 // `syntax error at or near "ISOLATION"`.
3025 "transaction"
3026 if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("isolation"))
3027 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("level")) =>
3028 {
3029 self.advance(); // ISOLATION
3030 self.advance(); // LEVEL
3031 Ok(Statement::ShowParameter(
3032 "transaction_isolation".to_string(),
3033 ))
3034 }
3035 // v7.40.12 — PG's two-word spelling of the
3036 // `timezone` GUC, and the one psql's own `\timing`
3037 // era documentation and every ORM's dialect probe
3038 // send. Until now only the pgwire host recognised
3039 // it, in a shortcut that answered SHOW from a copy;
3040 // removing that shortcut moved the spelling here,
3041 // where the embedded API gets it too. Same shape as
3042 // the isolation-level arm above: `time` is a bare
3043 // ident, so without this the statement ended there
3044 // and `ZONE` was a syntax error.
3045 "time"
3046 if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("zone")) =>
3047 {
3048 self.advance(); // ZONE
3049 Ok(Statement::ShowParameter("timezone".to_string()))
3050 }
3051 // v7.40.12 — `SHOW SESSION AUTHORIZATION`, PG's
3052 // spelling of the login identity. `session` is a bare
3053 // ident, so the statement ended there and `AUTHORIZATION`
3054 // was a syntax error; the engine already knows the
3055 // answer, as `SELECT session_user` on the same
3056 // connection shows.
3057 "session"
3058 if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("authorization")) =>
3059 {
3060 self.advance(); // AUTHORIZATION
3061 Ok(Statement::ShowParameter(
3062 "session_authorization".to_string(),
3063 ))
3064 }
3065 other => {
3066 // v7.38 (read01 P3.20) — a custom namespaced GUC
3067 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3068 // consume the dotted tail so it round-trips with
3069 // `SET app.foo` / `current_setting('app.foo')`.
3070 let mut full = other.to_string();
3071 while matches!(self.peek(), Token::Dot) {
3072 self.advance();
3073 let seg = self.expect_ident_like()?;
3074 full.push('.');
3075 full.push_str(&seg.to_ascii_lowercase());
3076 }
3077 Ok(Statement::ShowParameter(full))
3078 }
3079 }
3080 }
3081 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3082 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3083 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3084 // arrived as a bare ident; tokenising it dedicatedly
3085 // keeps the dispatch tree small.
3086 Token::Drop => {
3087 self.advance();
3088 match self.peek() {
3089 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3090 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3091 // around DROP ROLE cleanup. SPG has no role-owner
3092 // model, so consume to boundary as a no-op.
3093 Token::Ident(s) | Token::QuotedIdent(s)
3094 if s.eq_ignore_ascii_case("owned") =>
3095 {
3096 // v7.39 (round 696) — still a no-op (SPG has no
3097 // role-owner model), but the ROLE is carried out so
3098 // the engine can refuse one that does not exist,
3099 // which is what PG18 does.
3100 self.advance();
3101 if self.peek_is_by() {
3102 self.advance();
3103 }
3104 let names = self.take_comma_separated_names();
3105 self.consume_until_statement_boundary();
3106 Ok(Statement::ValidateOnly {
3107 kind: crate::ast::ValidateOnlyKind::RoleName,
3108 names,
3109 })
3110 }
3111 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3112 // It drops only a TEMPORARY table, and name resolution
3113 // already prefers the session's own, so the keyword is
3114 // consumed and the ordinary DROP TABLE path runs.
3115 Token::Ident(s) | Token::QuotedIdent(s)
3116 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3117 {
3118 self.advance();
3119 if !matches!(self.peek(), Token::Table) {
3120 return Err(self.err(alloc::format!(
3121 "expected TABLE after DROP TEMPORARY, got {:?}",
3122 self.peek()
3123 )));
3124 }
3125 self.parse_drop_table_after_keyword()
3126 }
3127 Token::Publication => {
3128 self.advance();
3129 // v7.39 (round 754, F31-B4) — the round-753
3130 // audit probe tripped over the missing
3131 // `IF EXISTS` here (syntax error).
3132 let if_exists = self.consume_if_exists();
3133 let name = self.expect_ident_or_string()?;
3134 Ok(Statement::DropPublication { name, if_exists })
3135 }
3136 Token::Subscription => {
3137 self.advance();
3138 let if_exists = self.consume_if_exists();
3139 let name = self.expect_ident_or_string()?;
3140 Ok(Statement::DropSubscription { name, if_exists })
3141 }
3142 Token::Ident(s) | Token::QuotedIdent(s)
3143 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3144 {
3145 self.advance();
3146 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3147 // login user IS a role in PG, and SPG's store holds
3148 // both. `IF EXISTS` is accepted on either spelling.
3149 let if_exists = self.consume_if_exists();
3150 let name = self.expect_ident_or_string()?;
3151 Ok(Statement::DropUser { name, if_exists })
3152 }
3153 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3154 // CREATE DATABASE has parsed since v7.14 and this did
3155 // not, so `DROP DATABASE IF EXISTS x` — what every
3156 // teardown script and pg_dumpall preamble opens with —
3157 // came back as a syntax error, which IF EXISTS cannot
3158 // soften. The name is carried so the engine can answer
3159 // the way PG does; PG never lets this succeed on a
3160 // single-database server, since the name is either
3161 // unknown ("database … does not exist", or a notice
3162 // under IF EXISTS) or the one you are connected to
3163 // ("cannot drop the currently open database").
3164 Token::Ident(s) | Token::QuotedIdent(s)
3165 if s.eq_ignore_ascii_case("database") =>
3166 {
3167 self.advance();
3168 let if_exists = self.consume_if_exists();
3169 let name = self.expect_ident_or_string()?;
3170 self.consume_until_statement_boundary();
3171 Ok(Statement::DropDatabase { name, if_exists })
3172 }
3173 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3174 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3175 self.advance();
3176 let if_exists = self.consume_if_exists();
3177 let name = self.expect_ident_like()?;
3178 // ON <table>
3179 if !matches!(self.peek(), Token::On) {
3180 return Err(self.err(alloc::format!(
3181 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3182 self.peek()
3183 )));
3184 }
3185 self.advance();
3186 let table = self.expect_ident_like()?;
3187 Ok(Statement::DropTrigger {
3188 name,
3189 table,
3190 if_exists,
3191 })
3192 }
3193 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3194 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3195 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3196 self.advance();
3197 let if_exists = self.consume_if_exists();
3198 let name = self.expect_ident_like()?;
3199 if !matches!(self.peek(), Token::On) {
3200 return Err(self.err(alloc::format!(
3201 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3202 self.peek()
3203 )));
3204 }
3205 self.advance();
3206 let table = self.expect_ident_like()?;
3207 // Optional CASCADE / RESTRICT — accepted, no effect.
3208 self.consume_until_statement_boundary();
3209 Ok(Statement::DropRule {
3210 name,
3211 table,
3212 if_exists,
3213 })
3214 }
3215 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3216 // v7.12.4 ignores any optional arg-list (signature-
3217 // based overload disambiguation lands in v7.12.5+).
3218 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3219 self.advance();
3220 let if_exists = self.consume_if_exists();
3221 let name = self.expect_ident_like()?;
3222 // v7.39 (read01 round 62) — the argument list identifies
3223 // WHICH overload to drop, so it is captured, not
3224 // discarded. `DROP FUNCTION f` (no list) is legal when
3225 // the name is unambiguous; the engine enforces that.
3226 let args = if matches!(self.peek(), Token::LParen) {
3227 Some(self.parse_function_signature_types()?)
3228 } else {
3229 None
3230 };
3231 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3232 // trailer, which `DROP TABLE` and `DROP INDEX` have
3233 // accepted since v7.14 and this one refused outright.
3234 // pg_dump writes it, so refusing was a parse error in
3235 // the middle of a restore. SPG drops the function
3236 // either way — it tracks no dependents to cascade to —
3237 // which is the same reading the other two give it.
3238 self.consume_drop_behaviour();
3239 Ok(Statement::DropFunction {
3240 name,
3241 args,
3242 if_exists,
3243 })
3244 }
3245 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3246 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3247 // emit DROP TABLE IF EXISTS at the head of every
3248 // CREATE TABLE block so re-importing a dump
3249 // overwrites prior state. SPG accepts and removes
3250 // matching tables; CASCADE/RESTRICT trailers
3251 // accepted silently.
3252 Token::Table => self.parse_drop_table_after_keyword(),
3253 // v7.14.0 — DROP INDEX [IF EXISTS] name
3254 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3255 // for partial-index renames and pgvector
3256 // migrations. SPG removes the matching index;
3257 // IF EXISTS makes the drop idempotent.
3258 Token::Index => {
3259 self.advance();
3260 let if_exists_at = self.pos;
3261 let if_exists = self.consume_if_exists();
3262 let name = self.expect_ident_like()?;
3263 // v7.39.7 — MySQL's own spelling, which SPG
3264 // refused.
3265 //
3266 // `DROP INDEX i ON t` is how MySQL drops an
3267 // index; its names live inside a table, so the
3268 // statement names the table. Measured against
3269 // MySQL 9.7.2: the form above works, and the
3270 // bare `DROP INDEX i` PostgreSQL uses is a 1064
3271 // there. SPG had it exactly backwards on the
3272 // MySQL wire — the bare form accepted, MySQL's
3273 // own a syntax error — so a migration that drops
3274 // an index failed against the drop-in and not
3275 // against the thing it replaces.
3276 let table = if matches!(self.peek(), Token::On) {
3277 self.advance();
3278 Some(self.expect_ident_like()?)
3279 } else {
3280 None
3281 };
3282 if self.mysql_dialect {
3283 // MySQL has no `IF EXISTS` here either:
3284 // `DROP INDEX IF EXISTS i ON t` is a 1064.
3285 if if_exists {
3286 return Err(self.err_at(
3287 if_exists_at,
3288 "MySQL has no IF EXISTS on DROP INDEX".into(),
3289 ));
3290 }
3291 if table.is_none() {
3292 return Err(self.err("expected ON after the index name".into()));
3293 }
3294 }
3295 if matches!(
3296 self.peek(),
3297 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3298 || s.eq_ignore_ascii_case("restrict")
3299 ) {
3300 self.advance();
3301 }
3302 Ok(Statement::DropIndex {
3303 name,
3304 if_exists,
3305 table,
3306 })
3307 }
3308 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3309 // [CASCADE|RESTRICT]. SPG is single-database;
3310 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3311 // name [, name…] [CASCADE | RESTRICT]. Real
3312 // unregister (was silent no-op pre-v7.17).
3313 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3314 self.advance();
3315 let if_exists = self.consume_if_exists();
3316 let mut names = vec![self.expect_ident_like()?];
3317 while matches!(self.peek(), Token::Comma) {
3318 self.advance();
3319 names.push(self.expect_ident_like()?);
3320 }
3321 if matches!(
3322 self.peek(),
3323 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3324 || s.eq_ignore_ascii_case("restrict")
3325 ) {
3326 self.advance();
3327 }
3328 Ok(Statement::DropSchema { names, if_exists })
3329 }
3330 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3331 // name [, name…] [CASCADE|RESTRICT].
3332 Token::Ident(s) | Token::QuotedIdent(s)
3333 if s.eq_ignore_ascii_case("type") =>
3334 {
3335 self.advance();
3336 let if_exists = self.consume_if_exists();
3337 let mut names = vec![self.expect_ident_like()?];
3338 while matches!(self.peek(), Token::Comma) {
3339 self.advance();
3340 names.push(self.expect_ident_like()?);
3341 }
3342 if matches!(
3343 self.peek(),
3344 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3345 || s.eq_ignore_ascii_case("restrict")
3346 ) {
3347 self.advance();
3348 }
3349 Ok(Statement::DropType { names, if_exists })
3350 }
3351 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3352 // name [, name…] [CASCADE|RESTRICT].
3353 Token::Ident(s) | Token::QuotedIdent(s)
3354 if s.eq_ignore_ascii_case("domain") =>
3355 {
3356 self.advance();
3357 let if_exists = self.consume_if_exists();
3358 let mut names = vec![self.expect_ident_like()?];
3359 while matches!(self.peek(), Token::Comma) {
3360 self.advance();
3361 names.push(self.expect_ident_like()?);
3362 }
3363 if matches!(
3364 self.peek(),
3365 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3366 || s.eq_ignore_ascii_case("restrict")
3367 ) {
3368 self.advance();
3369 }
3370 Ok(Statement::DropDomain { names, if_exists })
3371 }
3372 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3373 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3374 Token::Ident(s) | Token::QuotedIdent(s)
3375 if s.eq_ignore_ascii_case("materialized") =>
3376 {
3377 self.advance();
3378 let nxt = self.peek().clone();
3379 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3380 {
3381 return Err(self.err(alloc::format!(
3382 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3383 )));
3384 }
3385 self.advance();
3386 let if_exists = self.consume_if_exists();
3387 let mut names = vec![self.expect_ident_like()?];
3388 while matches!(self.peek(), Token::Comma) {
3389 self.advance();
3390 names.push(self.expect_ident_like()?);
3391 }
3392 if matches!(
3393 self.peek(),
3394 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3395 || s.eq_ignore_ascii_case("restrict")
3396 ) {
3397 self.advance();
3398 }
3399 Ok(Statement::DropMaterializedView { names, if_exists })
3400 }
3401 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3402 // name [, name…] [CASCADE|RESTRICT].
3403 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3404 self.advance();
3405 let if_exists = self.consume_if_exists();
3406 let mut names = vec![self.expect_ident_like()?];
3407 while matches!(self.peek(), Token::Comma) {
3408 self.advance();
3409 names.push(self.expect_ident_like()?);
3410 }
3411 if matches!(
3412 self.peek(),
3413 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3414 || s.eq_ignore_ascii_case("restrict")
3415 ) {
3416 self.advance();
3417 }
3418 Ok(Statement::DropView { names, if_exists })
3419 }
3420 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3421 // [CASCADE|RESTRICT]. Real removal from catalog
3422 // (was a silent no-op pre-v7.17).
3423 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3424 self.advance();
3425 let if_exists = self.consume_if_exists();
3426 let mut names = vec![self.expect_ident_like()?];
3427 while matches!(self.peek(), Token::Comma) {
3428 self.advance();
3429 names.push(self.expect_ident_like()?);
3430 }
3431 if matches!(
3432 self.peek(),
3433 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3434 || s.eq_ignore_ascii_case("restrict")
3435 ) {
3436 self.advance();
3437 }
3438 Ok(Statement::DropSequence { names, if_exists })
3439 }
3440 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3441 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3442 self.advance();
3443 self.parse_drop_policy_after_keyword()
3444 }
3445 // v7.37.17 (17.6 siblings) — DROP <target> for
3446 // targets SPG doesn't natively track. pg_dump
3447 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3448 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3449 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3450 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3451 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3452 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3453 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3454 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3455 // etc. — accept + Empty-return so pg_dump tails
3456 // load through. Materialized-view drop dispatches
3457 // to the existing DropTable path when the token
3458 // is Materialized-View-shaped (elsewhere in
3459 // this parser).
3460 Token::Ident(s) | Token::QuotedIdent(s)
3461 if s.eq_ignore_ascii_case("text")
3462 // The DROP dispatch matches on PEEK — `text` is
3463 // not yet consumed, so SEARCH/CONFIGURATION sit
3464 // at pos+1/pos+2 (the round-695 trap's mirror).
3465 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3466 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3467 {
3468 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3469 // validates the name; DICTIONARY / PARSER / TEMPLATE
3470 // stay in the noise arm below.
3471 self.advance(); // TEXT
3472 self.advance(); // SEARCH
3473 self.advance(); // CONFIGURATION
3474 let if_exists = self.consume_if_exists();
3475 let names = self.take_comma_separated_names();
3476 self.consume_until_statement_boundary();
3477 if if_exists {
3478 return Ok(Statement::Empty);
3479 }
3480 Ok(Statement::ValidateOnly {
3481 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3482 names,
3483 })
3484 }
3485 Token::Ident(s) | Token::QuotedIdent(s)
3486 if matches!(
3487 s.to_ascii_lowercase().as_str(),
3488 "type"
3489 | "domain"
3490 | "operator"
3491 | "cast"
3492 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3493 // TEMPLATE (CONFIGURATION intercepted above).
3494 | "text"
3495 | "materialized"
3496 | "large"
3497 | "role"
3498 | "access"
3499 | "procedure"
3500 | "routine"
3501 ) =>
3502 {
3503 self.consume_until_statement_boundary();
3504 Ok(Statement::Empty)
3505 }
3506 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3507 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3508 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3509 // foreign-data warning family (round 706) so a
3510 // CREATE→DROP sequence in a dump stays consistent.
3511 Token::Ident(s) | Token::QuotedIdent(s)
3512 if s.eq_ignore_ascii_case("server")
3513 || s.eq_ignore_ascii_case("foreign") =>
3514 {
3515 self.advance();
3516 self.consume_until_statement_boundary();
3517 Ok(Statement::ValidateOnly {
3518 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3519 names: Vec::new(),
3520 })
3521 }
3522 Token::Ident(s) | Token::QuotedIdent(s)
3523 if s.eq_ignore_ascii_case("collation")
3524 || s.eq_ignore_ascii_case("tablespace") =>
3525 {
3526 let kind = if s.eq_ignore_ascii_case("collation") {
3527 crate::ast::ValidateOnlyKind::CollationName
3528 } else {
3529 crate::ast::ValidateOnlyKind::TablespaceName
3530 };
3531 self.advance();
3532 let if_exists = self.consume_if_exists();
3533 let names = self.take_comma_separated_names();
3534 self.consume_until_statement_boundary();
3535 if if_exists {
3536 return Ok(Statement::Empty);
3537 }
3538 Ok(Statement::ValidateOnly { kind, names })
3539 }
3540 Token::Ident(s) | Token::QuotedIdent(s)
3541 if s.eq_ignore_ascii_case("event") =>
3542 {
3543 self.advance();
3544 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3545 {
3546 self.advance();
3547 }
3548 let if_exists = self.consume_if_exists();
3549 let names = self.take_comma_separated_names();
3550 self.consume_until_statement_boundary();
3551 if if_exists {
3552 return Ok(Statement::Empty);
3553 }
3554 Ok(Statement::ValidateOnly {
3555 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3556 names,
3557 })
3558 }
3559 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3560 // leave the noise list; see the ValidateOnly kinds.
3561 Token::Ident(s) | Token::QuotedIdent(s)
3562 if s.eq_ignore_ascii_case("conversion")
3563 || s.eq_ignore_ascii_case("language")
3564 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3565 // FIRST — the first draft looked for it after.
3566 || s.eq_ignore_ascii_case("procedural") =>
3567 {
3568 let kind = if s.eq_ignore_ascii_case("conversion") {
3569 crate::ast::ValidateOnlyKind::ConversionName
3570 } else {
3571 crate::ast::ValidateOnlyKind::LanguageName
3572 };
3573 self.advance();
3574 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3575 {
3576 self.advance();
3577 }
3578 let if_exists = self.consume_if_exists();
3579 let names = self.take_comma_separated_names();
3580 self.consume_until_statement_boundary();
3581 if if_exists {
3582 return Ok(Statement::Empty);
3583 }
3584 Ok(Statement::ValidateOnly { kind, names })
3585 }
3586 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3587 // name(argtypes)[, …]`. Parsed for real so the engine
3588 // can answer as PG does; see Statement::DropAggregate.
3589 Token::Ident(s) | Token::QuotedIdent(s)
3590 if s.eq_ignore_ascii_case("aggregate") =>
3591 {
3592 self.advance();
3593 let if_exists = self.consume_if_exists();
3594 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3595 loop {
3596 let name = self.expect_ident_like()?;
3597 if !matches!(self.peek(), Token::LParen) {
3598 return Err(self.err(alloc::format!(
3599 "expected argument list after DROP AGGREGATE {name}"
3600 )));
3601 }
3602 self.advance();
3603 let mut args: Vec<String> = Vec::new();
3604 let mut star = false;
3605 loop {
3606 match self.peek().clone() {
3607 Token::RParen => {
3608 self.advance();
3609 break;
3610 }
3611 Token::Star => {
3612 self.advance();
3613 star = true;
3614 }
3615 Token::Comma => {
3616 self.advance();
3617 }
3618 _ => {
3619 // A type name may be multi-token
3620 // (`double precision`); glue idents
3621 // until , or ).
3622 let mut t = self.expect_ident_like()?;
3623 while let Token::Ident(nx) = self.peek() {
3624 let nx = nx.clone();
3625 self.advance();
3626 t.push(' ');
3627 t.push_str(&nx);
3628 }
3629 args.push(t);
3630 }
3631 }
3632 }
3633 items.push((name, if star { None } else { Some(args) }));
3634 if matches!(self.peek(), Token::Comma) {
3635 self.advance();
3636 } else {
3637 break;
3638 }
3639 }
3640 self.consume_until_statement_boundary();
3641 Ok(Statement::DropAggregate { if_exists, items })
3642 }
3643 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3644 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3645 // installed; `IF EXISTS` is the spelling that says do
3646 // not, and it keeps the no-op.
3647 Token::Ident(s) | Token::QuotedIdent(s)
3648 if s.eq_ignore_ascii_case("extension") =>
3649 {
3650 self.advance();
3651 let if_exists = self.consume_if_exists();
3652 let names = self.take_comma_separated_names();
3653 self.consume_until_statement_boundary();
3654 if if_exists {
3655 return Ok(Statement::Empty);
3656 }
3657 Ok(Statement::ValidateOnly {
3658 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3659 names,
3660 })
3661 }
3662 Token::Ident(s) | Token::QuotedIdent(s)
3663 if s.eq_ignore_ascii_case("statistics") =>
3664 {
3665 self.parse_drop_statistics_after_drop()
3666 }
3667 other => Err(self.err(format!(
3668 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3669 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3670 ))),
3671 }
3672 }
3673 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3674 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3675 // and accepted before the view name. SPG materialised
3676 // views re-evaluate on read (always-fresh semantics), so
3677 // the CONCURRENTLY-vs-serial distinction has no runtime
3678 // effect — the refresh body does not block readers either
3679 // way. Same accept-and-no-op pattern as DETACH PARTITION
3680 // CONCURRENTLY (16.5).
3681 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3682 self.advance();
3683 let nxt = self.peek().clone();
3684 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3685 {
3686 return Err(self.err(alloc::format!(
3687 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3688 )));
3689 }
3690 self.advance();
3691 let nxt2 = self.peek().clone();
3692 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3693 {
3694 return Err(self.err(alloc::format!(
3695 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3696 )));
3697 }
3698 self.advance();
3699 // Optional CONCURRENTLY noise word — consumed without
3700 // changing semantics.
3701 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3702 {
3703 self.advance();
3704 }
3705 let name = self.expect_ident_like()?;
3706 let with_data = self.parse_optional_with_data(true)?;
3707 Ok(Statement::RefreshMaterializedView { name, with_data })
3708 }
3709 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3710 self.advance();
3711 self.parse_update_after_keyword()
3712 }
3713 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3714 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3715 // [CASCADE | RESTRICT]. Clears every row from each named
3716 // table. Parses at the top level; the engine dispatcher
3717 // walks Statement::Truncate.
3718 // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3719 //
3720 // PostgreSQL renames a table through `ALTER TABLE … RENAME
3721 // TO`, which SPG already had, so this spelling answered 1064
3722 // — and it is what a MySQL migration writes. Measured on
3723 // 9.7.2: several pairs in one statement are accepted, and
3724 // renaming onto a name that exists is 1050.
3725 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3726 self.advance();
3727 if matches!(self.peek(), Token::Table) {
3728 self.advance();
3729 }
3730 let mut pairs: Vec<(String, String)> = Vec::new();
3731 loop {
3732 let from = self.expect_ident_like()?;
3733 if matches!(self.peek(), Token::To) {
3734 self.advance();
3735 } else {
3736 self.expect_keyword_ident("to")?;
3737 }
3738 let to = self.expect_ident_like()?;
3739 pairs.push((from, to));
3740 if matches!(self.peek(), Token::Comma) {
3741 self.advance();
3742 } else {
3743 break;
3744 }
3745 }
3746 Ok(Statement::RenameTables(pairs))
3747 }
3748 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3749 self.advance();
3750 // Optional TABLE noise word — PG accepts both the reserved
3751 // token and the bare identifier spelling.
3752 if matches!(self.peek(), Token::Table)
3753 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3754 {
3755 self.advance();
3756 }
3757 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3758 // not absorbed. The lookahead keeps a table genuinely
3759 // called `only` working: the keyword is a keyword only
3760 // when a name follows it.
3761 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3762 if s.eq_ignore_ascii_case("only"))
3763 && matches!(
3764 self.tokens.get(self.pos + 1),
3765 Some(Token::Ident(_) | Token::QuotedIdent(_))
3766 );
3767 if only {
3768 self.advance();
3769 }
3770 // Table names (comma-separated).
3771 let mut tables = Vec::new();
3772 loop {
3773 tables.push(self.expect_ident_like()?);
3774 if matches!(self.peek(), Token::Comma) {
3775 self.advance();
3776 continue;
3777 }
3778 break;
3779 }
3780 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3781 let mut restart_identity = false;
3782 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3783 {
3784 self.advance();
3785 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3786 {
3787 self.advance();
3788 restart_identity = true;
3789 }
3790 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3791 {
3792 self.advance();
3793 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3794 {
3795 self.advance();
3796 }
3797 }
3798 // Optional CASCADE / RESTRICT.
3799 let mut cascade = false;
3800 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3801 {
3802 self.advance();
3803 cascade = true;
3804 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3805 {
3806 self.advance();
3807 }
3808 Ok(Statement::Truncate {
3809 tables,
3810 restart_identity,
3811 cascade,
3812 only,
3813 })
3814 }
3815 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3816 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3817 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3818 // rows change so the index tree is always up-to-date;
3819 // REINDEX is a strict no-op. Accept the whole statement
3820 // shape to boundary for pg_dump round-trip compatibility.
3821 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3822 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3823 // index bloat to rebuild, so the work stays a no-op, but PG
3824 // validates what it was pointed at and this swallowed the
3825 // name at parse time — `REINDEX TABLE typo` reported
3826 // success. Measured on PG18: INDEX / TABLE name a relation,
3827 // SCHEMA a schema, SYSTEM nothing.
3828 self.advance();
3829 self.parse_reindex_tail()
3830 }
3831 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3832 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3833 // SPG has no MVCC bloat today (Phase D visibility map
3834 // queues with v7.38); the freezer collapses hot-tier
3835 // rows into cold segments automatically. VACUUM is a
3836 // no-op — pg_dump maintenance scripts and Discourse's
3837 // periodic-maintenance path both emit it.
3838 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3839 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3840 // actual bloat, so the pre-MVCC accept-and-ignore posture
3841 // became a silent no-op on a customer's manual reclaim.
3842 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3843 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3844 // ANALYZE is captured, the optional table name is captured.
3845 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3846 self.advance();
3847 // Parenthesised option list: absorb it.
3848 if matches!(self.peek(), Token::LParen) {
3849 let mut depth = 0usize;
3850 loop {
3851 match self.advance() {
3852 Token::LParen => depth += 1,
3853 Token::RParen => {
3854 depth -= 1;
3855 if depth == 0 {
3856 break;
3857 }
3858 }
3859 Token::Eof => break,
3860 _ => {}
3861 }
3862 }
3863 }
3864 let mut analyze = false;
3865 let mut table: Option<String> = None;
3866 loop {
3867 match self.peek() {
3868 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3869 // an identifier, so the loop below broke out on it and
3870 // dropped the table name: `VACUUM FULL nosuch` was
3871 // accepted where `VACUUM nosuch` was refused.
3872 Token::Full => {
3873 self.advance();
3874 }
3875 Token::Ident(w) | Token::QuotedIdent(w) => {
3876 let wl = w.to_ascii_lowercase();
3877 match wl.as_str() {
3878 "full" | "freeze" | "verbose" => {
3879 self.advance();
3880 }
3881 "analyze" | "analyse" => {
3882 analyze = true;
3883 self.advance();
3884 }
3885 _ => {
3886 table = Some(self.expect_ident_like()?);
3887 break;
3888 }
3889 }
3890 }
3891 _ => break,
3892 }
3893 }
3894 // Optional trailing column list / anything else to the
3895 // statement boundary (PG accepts per-column ANALYZE).
3896 self.consume_until_statement_boundary();
3897 Ok(Statement::Vacuum { table, analyze })
3898 }
3899 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3900 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3901 // <index>. PG stores rows in physical order matching
3902 // an index; SPG's hot-tier is append-only + cold-tier
3903 // is segment-frozen, so clustering has no persistent
3904 // effect. Accept-and-no-op for pg_dump compat.
3905 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3906 // v7.39 (round 535) — same as REINDEX above: the relation is
3907 // carried so the engine can refuse one that does not exist.
3908 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3909 self.advance();
3910 self.parse_cluster_tail()
3911 }
3912 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3913 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3914 // optional string payload; UNLISTEN takes a channel or `*`.
3915 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3916 self.advance();
3917 let ch = match self.advance() {
3918 Token::Ident(c) | Token::QuotedIdent(c) => c,
3919 other => {
3920 return Err(self.err(format!(
3921 "expected channel name after LISTEN, got {other:?}"
3922 )));
3923 }
3924 };
3925 Ok(Statement::Listen(ch))
3926 }
3927 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3928 self.advance();
3929 let channel = match self.advance() {
3930 Token::Ident(c) | Token::QuotedIdent(c) => c,
3931 other => {
3932 return Err(self.err(format!(
3933 "expected channel name after NOTIFY, got {other:?}"
3934 )));
3935 }
3936 };
3937 let payload = if matches!(self.peek(), Token::Comma) {
3938 self.advance();
3939 match self.advance() {
3940 Token::String(p) => Some(p),
3941 other => {
3942 return Err(self.err(format!(
3943 "expected string payload after NOTIFY <channel>, got {other:?}"
3944 )));
3945 }
3946 }
3947 } else {
3948 None
3949 };
3950 Ok(Statement::Notify { channel, payload })
3951 }
3952 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3953 self.advance();
3954 match self.advance() {
3955 Token::Star => Ok(Statement::Unlisten(None)),
3956 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3957 other => Err(self.err(format!(
3958 "expected channel name or * after UNLISTEN, got {other:?}"
3959 ))),
3960 }
3961 }
3962 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3963 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3964 // process-wide write lock today; explicit LOCK has no
3965 // effect. Accept-and-no-op for pg_dump / migration
3966 // compat.
3967 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3968 self.advance();
3969 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3970 // engine holds a process-wide write lock), but the TABLE
3971 // NAME is now carried out so the engine can refuse one that
3972 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3973 // READ|WRITE` is a different statement with the same first
3974 // word; it keeps the old no-op, because a MySQL dump's
3975 // bracket names tables it is about to create.
3976 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3977 if k.eq_ignore_ascii_case("tables"));
3978 if mysql_tables {
3979 self.consume_until_statement_boundary();
3980 return Ok(Statement::Empty);
3981 }
3982 if matches!(self.peek(), Token::Table) {
3983 self.advance();
3984 }
3985 let names = self.take_comma_separated_names();
3986 self.consume_until_statement_boundary();
3987 Ok(Statement::ValidateOnly {
3988 kind: crate::ast::ValidateOnlyKind::LockTable,
3989 names,
3990 })
3991 }
3992 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3993 // durability marker + snapshot in PG. SPG has WAL
3994 // checkpointing on a byte / time schedule (v7.37.10
3995 // 60s / 4 MiB defaults). The bare statement parses to
3996 // `Statement::Empty` here (the no_std engine owns no
3997 // WAL / snapshot); v7.37 Epic Du wires the HOST
3998 // (embedded `Database::execute_buffered`, via
3999 // `sql_is_checkpoint`) to force an immediate synchronous
4000 // checkpoint through `Database::checkpoint` — a real
4001 // durability barrier, matching PG.
4002 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
4003 self.advance();
4004 self.consume_until_statement_boundary();
4005 Ok(Statement::Empty)
4006 }
4007 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
4008 self.advance();
4009 self.parse_delete_after_keyword()
4010 }
4011 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
4012 // ALTER is not a reserved keyword in the lexer — handled
4013 // as a bare ident here.
4014 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
4015 self.advance();
4016 self.parse_alter_after_keyword()
4017 }
4018 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
4019 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
4020 // additions needed.
4021 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
4022 self.advance();
4023 self.parse_wait_after_keyword()
4024 }
4025 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
4026 // Bare ANALYZE → analyse every user table; ANALYZE
4027 // <name> → re-stats one. The argument is an optional
4028 // ident (or quoted ident); anything else is a parse
4029 // error.
4030 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
4031 // `WHERE` filter (carved out per V6_7_DESIGN.md
4032 // STABILITY). Lex order: identifier "compact" → "cold"
4033 // → "segments". Anything else after `COMPACT` is a
4034 // parse error.
4035 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
4036 self.advance();
4037 let next = self.peek().clone();
4038 let cold = match next {
4039 Token::Ident(s) | Token::QuotedIdent(s) => s,
4040 _ => {
4041 return Err(
4042 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
4043 );
4044 }
4045 };
4046 if !cold.eq_ignore_ascii_case("cold") {
4047 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
4048 }
4049 self.advance();
4050 let next = self.peek().clone();
4051 let segments = match next {
4052 Token::Ident(s) | Token::QuotedIdent(s) => s,
4053 _ => {
4054 return Err(self.err(format!(
4055 "expected SEGMENTS after COMPACT COLD, got {:?}",
4056 self.peek()
4057 )));
4058 }
4059 };
4060 if !segments.eq_ignore_ascii_case("segments") {
4061 return Err(self.err(format!(
4062 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4063 )));
4064 }
4065 self.advance();
4066 Ok(Statement::CompactColdSegments)
4067 }
4068 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4069 // Parsed as a case-insensitive identifier since MERGE
4070 // isn't a reserved lexer keyword (collides with the
4071 // mysqldump `ALGORITHM = MERGE` view clause if it
4072 // were); the inner parser drives the rest of the
4073 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4074 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4075 self.advance();
4076 self.parse_merge_after_keyword()
4077 }
4078 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4079 self.advance();
4080 // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4081 // keyword is noise to the parse; what differs is the
4082 // ANSWER, which MySQL returns as a result set — see the
4083 // executor.
4084 let mysql_table_kw = matches!(self.peek(), Token::Table);
4085 if mysql_table_kw {
4086 self.advance();
4087 }
4088 let target = match self.peek() {
4089 Token::Eof | Token::Semicolon => None,
4090 Token::Ident(_) | Token::QuotedIdent(_) => {
4091 Some(self.expect_ident_like()?)
4092 }
4093 other => {
4094 return Err(self.err(format!(
4095 "expected table name or end of statement after ANALYZE, got {other:?}"
4096 )));
4097 }
4098 };
4099 // v7.39 (round 776, F31 J7) — the per-column form
4100 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4101 // here while the VACUUM arm already consumed it; SPG
4102 // analyzes whole tables, so the list parses and is
4103 // accepted like the VACUUM path's.
4104 if target.is_some() && matches!(self.peek(), Token::LParen) {
4105 self.advance();
4106 loop {
4107 let _ = self.expect_ident_like()?;
4108 match self.peek() {
4109 Token::Comma => {
4110 self.advance();
4111 }
4112 Token::RParen => {
4113 self.advance();
4114 break;
4115 }
4116 other => {
4117 return Err(self.err(format!(
4118 "expected ',' or ')' in ANALYZE column list, got {other:?}"
4119 )));
4120 }
4121 }
4122 }
4123 }
4124 Ok(Statement::Analyze(target))
4125 }
4126 // v7.12.1 — `SET <name> [TO|=] <value>`. The
4127 // `default_text_search_config` parameter is consumed
4128 // by the FTS function dispatcher; other parameter
4129 // names are recorded but treated as a no-op so PG
4130 // dump output loads.
4131 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4132 self.advance();
4133 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4134 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4135 // …` which the SessionVar path handles). `LOCAL` is the only
4136 // one that changes semantics — it scopes the change to the
4137 // current transaction — so capture it; SESSION / GLOBAL are
4138 // accepted and treated as the default session scope.
4139 let mut set_local = false;
4140 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4141 let q = s.to_ascii_lowercase();
4142 if q == "local" || q == "session" || q == "global" {
4143 set_local = q == "local";
4144 self.advance();
4145 }
4146 }
4147 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4148 // { DEFAULT | <role> }`. pg_dump's ACL section switches
4149 // to the object owner with it. SPG maps it onto the
4150 // session-role machinery (recorded delta RD-10: PG moves
4151 // session_user too; SPG moves the effective role).
4152 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4153 if s.eq_ignore_ascii_case("authorization"))
4154 {
4155 self.advance(); // AUTHORIZATION
4156 let role = match self.peek().clone() {
4157 Token::Default => {
4158 self.advance();
4159 None
4160 }
4161 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4162 self.advance();
4163 Some(s)
4164 }
4165 _ => None,
4166 };
4167 return Ok(Statement::SetRole(role));
4168 }
4169 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4170 // <collation>]` — change the connection client
4171 // charset. SPG stores UTF-8 always and orders
4172 // bytewise; accept as a no-op.
4173 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4174 {
4175 self.advance();
4176 // v7.39 — this used to parse the clause and throw it
4177 // away ("SPG stores UTF-8 always and orders
4178 // bytewise; accept as a no-op"). That sentence
4179 // stopped being true when collations arrived, and
4180 // once `collation_connection` began driving literal
4181 // comparison, dropping the COLLATE clause became a
4182 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4183 // utf8mb4_general_ci` reported back
4184 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4185 //
4186 // The charset name is emitted as `names` and the
4187 // ENGINE expands it, because which collation a
4188 // charset defaults to is MySQL semantics and belongs
4189 // beside the rest of them, not in the parser.
4190 let mut pairs = alloc::vec::Vec::new();
4191 if matches!(
4192 self.peek(),
4193 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4194 ) {
4195 let charset = match self.advance() {
4196 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4197 _ => unreachable!("peeked an ident-or-string"),
4198 };
4199 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4200 }
4201 // Optional `COLLATE <name>` — emitted AFTER `names`
4202 // so it overrides the charset's default, which is
4203 // what MySQL does.
4204 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4205 {
4206 self.advance();
4207 if matches!(
4208 self.peek(),
4209 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4210 ) {
4211 let coll = match self.advance() {
4212 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4213 _ => unreachable!("peeked an ident-or-string"),
4214 };
4215 pairs.push((
4216 String::from("collation_connection"),
4217 crate::ast::SetValue::Ident(coll),
4218 ));
4219 }
4220 }
4221 if pairs.is_empty() {
4222 return Ok(Statement::Empty);
4223 }
4224 return Ok(Statement::SetParameterList(pairs));
4225 }
4226 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4227 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4228 // uses this to switch to the object owner before
4229 // recreating tables. SPG has no role system so this
4230 // is a no-op.
4231 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4232 {
4233 self.advance(); // ROLE
4234 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4235 // reset to the login identity; a name / string sets the
4236 // effective role that drives current_user + RLS.
4237 let role = match self.peek().clone() {
4238 Token::Default => {
4239 self.advance();
4240 None
4241 }
4242 Token::Ident(s) | Token::QuotedIdent(s)
4243 if s.eq_ignore_ascii_case("none") =>
4244 {
4245 self.advance();
4246 None
4247 }
4248 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4249 self.advance();
4250 Some(s)
4251 }
4252 _ => None,
4253 };
4254 return Ok(Statement::SetRole(role));
4255 }
4256 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4257 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4258 // ISO SQL surface). pg_dump prepends this to fix
4259 // the isolation level for the restore session. SPG
4260 // defaults to READ COMMITTED and doesn't yet honor
4261 // session-set isolation across statements — accept
4262 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4263 // per-tx form is handled elsewhere.
4264 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4265 {
4266 self.advance(); // CHARACTERISTICS
4267 if matches!(self.peek(), Token::As) {
4268 self.advance();
4269 }
4270 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4271 self.advance();
4272 }
4273 // v7.39 — no longer a no-op. The note above said SPG
4274 // "doesn't yet honor session-set isolation across
4275 // statements"; it does now, through
4276 // `default_transaction_isolation`, and measured on
4277 // PG 18.6 this statement is exactly a way to set it:
4278 //
4279 // SET SESSION CHARACTERISTICS AS TRANSACTION
4280 // ISOLATION LEVEL REPEATABLE READ;
4281 // current_setting('default_transaction_isolation')
4282 // -> repeatable read
4283 //
4284 // pg_dump prepends this to fix the level for a
4285 // restore session, so accepting it and doing nothing
4286 // meant the restore ran at a level nobody chose.
4287 //
4288 // v7.39 wired READ ONLY here for the same reason,
4289 // once something enforced it.
4290 //
4291 // v7.40.12 — and DEFERRABLE, once something honoured
4292 // it. This was the LAST of the three still being
4293 // consumed and dropped: measured on PG 18.6,
4294 // `SET SESSION CHARACTERISTICS AS TRANSACTION
4295 // DEFERRABLE; SHOW default_transaction_deferrable`
4296 // answers `on`, and SPG answered `off` while
4297 // answering the READ ONLY and ISOLATION LEVEL forms
4298 // of the same statement correctly.
4299 let modes = self.parse_isolation_level_clauses()?;
4300 self.consume_until_statement_boundary();
4301 let mut pairs: alloc::vec::Vec<(
4302 alloc::string::String,
4303 crate::ast::SetValue,
4304 )> = alloc::vec::Vec::new();
4305 if let Some(level) = modes.isolation {
4306 pairs.push((
4307 alloc::string::String::from("default_transaction_isolation"),
4308 crate::ast::SetValue::String(alloc::string::String::from(
4309 level.as_pg_str(),
4310 )),
4311 ));
4312 }
4313 if let Some(ro) = modes.read_only {
4314 pairs.push((
4315 alloc::string::String::from("default_transaction_read_only"),
4316 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4317 "on"
4318 } else {
4319 "off"
4320 })),
4321 ));
4322 }
4323 if let Some(d) = modes.deferrable {
4324 pairs.push((
4325 alloc::string::String::from("default_transaction_deferrable"),
4326 crate::ast::SetValue::Ident(alloc::string::String::from(if d {
4327 "on"
4328 } else {
4329 "off"
4330 })),
4331 ));
4332 }
4333 return Ok(if pairs.is_empty() {
4334 Statement::Empty
4335 } else {
4336 Statement::SetParameterList(pairs)
4337 });
4338 }
4339 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4340 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4341 // pg_dump emits this to control the deferrability of
4342 // FK / UNIQUE constraints across a bulk restore. SPG
4343 // has no deferrable-constraint machinery today; the
4344 // FK checker is strict-immediate. Accept-and-no-op
4345 // for pg_dump round-trip compatibility.
4346 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4347 {
4348 self.advance(); // CONSTRAINTS
4349 // v7.39 (round 288) — no longer a no-op: the trailing
4350 // DEFERRED / IMMEDIATE sets the transaction's timing.
4351 // v7.39 (round 308, V29) — and the names are kept.
4352 // They used to be skipped over on the way to the
4353 // DEFERRED keyword, so a named form silently behaved
4354 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4355 // every deferrable constraint in the transaction.
4356 let mut names: alloc::vec::Vec<alloc::string::String> =
4357 alloc::vec::Vec::new();
4358 if matches!(self.peek(), Token::All) {
4359 self.advance();
4360 } else {
4361 loop {
4362 let mut n = self.expect_ident_like()?;
4363 // A schema-qualified name (`public.fk_a`)
4364 // identifies the same constraint; PG resolves
4365 // it by the trailing segment.
4366 while matches!(self.peek(), Token::Dot) {
4367 self.advance();
4368 n = self.expect_ident_like()?;
4369 }
4370 names.push(n);
4371 if matches!(self.peek(), Token::Comma) {
4372 self.advance();
4373 } else {
4374 break;
4375 }
4376 }
4377 }
4378 let deferred = match self.peek() {
4379 Token::Ident(s) | Token::QuotedIdent(s)
4380 if s.eq_ignore_ascii_case("deferred") =>
4381 {
4382 true
4383 }
4384 Token::Ident(s) | Token::QuotedIdent(s)
4385 if s.eq_ignore_ascii_case("immediate") =>
4386 {
4387 false
4388 }
4389 other => {
4390 return Err(self.err(alloc::format!(
4391 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4392 )));
4393 }
4394 };
4395 self.advance();
4396 return Ok(Statement::SetConstraints { names, deferred });
4397 }
4398 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4399 // { DEFAULT | '<role>' | <ident> }` (mailrs
4400 // round-10 A.1). pg_dump preamble emits the
4401 // `DEFAULT` form to reset session authorization.
4402 //
4403 // v7.39 (round 697) — this said "SPG has no role system so
4404 // this is a strict no-op". SPG has had one since round 58;
4405 // the comment outlived it, and with it the reason a name
4406 // that is not a role was accepted here. It still switches
4407 // no authorization — what it does now is refuse a role
4408 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4409 // AUTHORIZATION` (handled by the RESET parser
4410 // elsewhere). Reference:
4411 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4412 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4413 {
4414 self.advance(); // AUTHORIZATION
4415 match self.peek().clone() {
4416 Token::Default => {
4417 self.advance();
4418 }
4419 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4420 self.advance();
4421 return Ok(Statement::ValidateOnly {
4422 kind: crate::ast::ValidateOnlyKind::SessionAuthorization,
4423 names: alloc::vec![r],
4424 });
4425 }
4426 other => {
4427 return Err(self.err(alloc::format!(
4428 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4429 )));
4430 }
4431 }
4432 return Ok(Statement::Empty);
4433 }
4434 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4435 // ISOLATION LEVEL { READ COMMITTED | READ
4436 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4437 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4438 // PG-standard surface. v7.37.8 accepts the syntax
4439 // and tracks the selected level on
4440 // `Engine::current_isolation_level()`; the actual
4441 // MVCC / SSI semantics implementation lands in
4442 // the 轴 4 isolation framework (separate train).
4443 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4444 // internally; SPG behaves the same (effectively
4445 // READ COMMITTED at every level today).
4446 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4447 {
4448 self.advance(); // TRANSACTION
4449 let modes = self.parse_isolation_level_clauses()?;
4450 return Ok(Statement::SetTransaction { modes });
4451 }
4452 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4453 // alias — same accept-as-no-op as SET NAMES.
4454 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4455 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4456 {
4457 self.advance(); // CHARACTER
4458 self.advance(); // SET
4459 if matches!(
4460 self.peek(),
4461 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4462 ) {
4463 self.advance();
4464 }
4465 return Ok(Statement::Empty);
4466 }
4467 // v7.39 (GUC) — PG spells the timezone GUC as two
4468 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4469 // where <value> is a string/ident or the LOCAL /
4470 // DEFAULT keyword (both mean "back to the default").
4471 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4472 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4473 {
4474 self.advance(); // TIME
4475 self.advance(); // ZONE
4476 let value = match self.peek().clone() {
4477 Token::Ident(s)
4478 if s.eq_ignore_ascii_case("local")
4479 || s.eq_ignore_ascii_case("default") =>
4480 {
4481 self.advance();
4482 crate::ast::SetValue::Default
4483 }
4484 Token::Default => {
4485 self.advance();
4486 crate::ast::SetValue::Default
4487 }
4488 _ => self.parse_set_value()?,
4489 };
4490 return Ok(Statement::SetParameter {
4491 name: "timezone".into(),
4492 value,
4493 local: set_local,
4494 });
4495 }
4496 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4497 // MySQL USER-variable assignment: its own per-session
4498 // namespace, an arbitrary expression on the right, and `:=`
4499 // as a second spelling of `=`. It used to fall into the
4500 // session-PARAMETER list below, whose values are literals and
4501 // whose store nothing reads back under a `@` name — so the
4502 // assignment reported success and vanished.
4503 //
4504 // A `@@`-prefixed LHS is a real engine setting and keeps the
4505 // old path.
4506 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4507 return self.parse_set_user_vars();
4508 }
4509 // v7.14.0 — multi-assignment form
4510 // `SET a = 1, b = 2, …`. Single-assignment is the
4511 // 1-element case. Each LHS may be a regular ident
4512 // or a SessionVar (`@VAR` / `@@VAR`).
4513 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4514 loop {
4515 let lhs = match self.peek().clone() {
4516 Token::SessionVar(s) => {
4517 self.advance();
4518 s
4519 }
4520 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4521 other => {
4522 return Err(self.err(format!(
4523 "expected parameter name after SET, got {other:?}"
4524 )));
4525 }
4526 };
4527 // Accept either `=` or the bare `TO` keyword.
4528 match self.peek() {
4529 Token::Eq => {
4530 self.advance();
4531 }
4532 Token::To => {
4533 self.advance();
4534 }
4535 other => {
4536 return Err(self.err(format!(
4537 "expected `=` or TO after SET {lhs}, got {other:?}"
4538 )));
4539 }
4540 }
4541 let mut value = self.parse_set_value()?;
4542 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4543 // `, name TO` continues a MySQL-style multi-assign,
4544 // anything else is a PG list VALUE
4545 // (`SET search_path = myschema, public`) folded into
4546 // one comma-joined string.
4547 while matches!(self.peek(), Token::Comma) {
4548 let is_assign = matches!(
4549 self.tokens.get(self.pos + 1),
4550 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4551 ) && matches!(
4552 self.tokens.get(self.pos + 2),
4553 Some(Token::Eq | Token::To)
4554 );
4555 if is_assign {
4556 break;
4557 }
4558 self.advance(); // comma
4559 let next = self.parse_set_value()?;
4560 let joined = alloc::format!(
4561 "{}, {}",
4562 set_value_text(&value),
4563 set_value_text(&next)
4564 );
4565 value = crate::ast::SetValue::String(joined);
4566 }
4567 pairs.push((lhs, value));
4568 if matches!(self.peek(), Token::Comma) {
4569 self.advance();
4570 continue;
4571 }
4572 break;
4573 }
4574 if pairs.len() == 1 {
4575 let (name, value) = pairs.into_iter().next().unwrap();
4576 Ok(Statement::SetParameter {
4577 name,
4578 value,
4579 local: set_local,
4580 })
4581 } else {
4582 Ok(Statement::SetParameterList(pairs))
4583 }
4584 }
4585 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4586 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4587 self.advance();
4588 match self.peek().clone() {
4589 Token::All => {
4590 self.advance();
4591 Ok(Statement::ResetParameter(None))
4592 }
4593 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4594 self.advance();
4595 Ok(Statement::ResetParameter(None))
4596 }
4597 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4598 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4599 self.advance();
4600 Ok(Statement::SetRole(None))
4601 }
4602 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4603 // (pg_dump's return from the owner switch).
4604 Token::Ident(s) | Token::QuotedIdent(s)
4605 if s.eq_ignore_ascii_case("session")
4606 && matches!(
4607 self.tokens.get(self.pos + 1),
4608 Some(Token::Ident(a) | Token::QuotedIdent(a))
4609 if a.eq_ignore_ascii_case("authorization")
4610 ) =>
4611 {
4612 self.advance(); // SESSION
4613 self.advance(); // AUTHORIZATION
4614 Ok(Statement::SetRole(None))
4615 }
4616 _ => {
4617 let name = self.parse_set_param_name()?;
4618 Ok(Statement::ResetParameter(Some(name)))
4619 }
4620 }
4621 }
4622 // v7.39 (round 218) — server-side cursors.
4623 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4624 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4625 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4626 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4627 self.advance();
4628 match self.peek().clone() {
4629 Token::All => {
4630 self.advance();
4631 Ok(Statement::CloseCursor { name: None })
4632 }
4633 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4634 self.advance();
4635 Ok(Statement::CloseCursor { name: None })
4636 }
4637 Token::Ident(n) | Token::QuotedIdent(n) => {
4638 self.advance();
4639 Ok(Statement::CloseCursor { name: Some(n) })
4640 }
4641 other => Err(self.err(format!(
4642 "expected cursor name or ALL after CLOSE, got {other:?}"
4643 ))),
4644 }
4645 }
4646 other => Err(self.err(format!(
4647 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4648 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4649 ))),
4650 }
4651 }
4652
4653 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4654 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4655 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4656 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4657 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4658 self.advance(); // DECLARE
4659 let name = match self.advance() {
4660 Token::Ident(n) | Token::QuotedIdent(n) => n,
4661 other => {
4662 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4663 }
4664 };
4665 let mut scroll: Option<bool> = None;
4666 loop {
4667 match self.peek() {
4668 Token::Ident(s)
4669 if s.eq_ignore_ascii_case("binary")
4670 || s.eq_ignore_ascii_case("insensitive")
4671 || s.eq_ignore_ascii_case("asensitive") =>
4672 {
4673 self.advance();
4674 }
4675 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4676 self.advance();
4677 scroll = Some(true);
4678 }
4679 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4680 {
4681 self.advance(); // NO
4682 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4683 return Err(self.err(format!(
4684 "expected SCROLL after NO in DECLARE, got {:?}",
4685 self.peek()
4686 )));
4687 }
4688 self.advance();
4689 scroll = Some(false);
4690 }
4691 _ => break,
4692 }
4693 }
4694 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4695 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4696 }
4697 self.advance();
4698 let mut hold = false;
4699 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4700 self.advance();
4701 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4702 return Err(self.err(format!(
4703 "expected HOLD after WITH in DECLARE, got {:?}",
4704 self.peek()
4705 )));
4706 }
4707 self.advance();
4708 hold = true;
4709 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4710 self.advance();
4711 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4712 return Err(self.err(format!(
4713 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4714 self.peek()
4715 )));
4716 }
4717 self.advance();
4718 }
4719 if !matches!(self.peek(), Token::For) {
4720 return Err(self.err(format!(
4721 "expected FOR before the cursor query, got {:?}",
4722 self.peek()
4723 )));
4724 }
4725 self.advance();
4726 let query = self.parse_one_statement()?;
4727 Ok(Statement::DeclareCursor {
4728 name,
4729 scroll,
4730 hold,
4731 query: alloc::boxed::Box::new(query),
4732 })
4733 }
4734
4735 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4736 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4737 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4738 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4739 use crate::ast::CursorDirection as D;
4740 self.advance(); // FETCH / MOVE
4741 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4742 let neg = if matches!(this.peek(), Token::Minus) {
4743 this.advance();
4744 true
4745 } else {
4746 false
4747 };
4748 match this.advance() {
4749 Token::Integer(v) => Ok(if neg { -v } else { v }),
4750 other => Err(this.err(format!("expected count, got {other:?}"))),
4751 }
4752 };
4753 let direction = match self.peek().clone() {
4754 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4755 self.advance();
4756 D::Next
4757 }
4758 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4759 self.advance();
4760 D::Prior
4761 }
4762 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4763 self.advance();
4764 D::First
4765 }
4766 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4767 self.advance();
4768 D::Last
4769 }
4770 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4771 self.advance();
4772 D::Absolute(signed_count(self)?)
4773 }
4774 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4775 self.advance();
4776 D::Relative(signed_count(self)?)
4777 }
4778 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4779 self.advance();
4780 match self.peek().clone() {
4781 Token::All => {
4782 self.advance();
4783 D::All
4784 }
4785 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4786 self.advance();
4787 D::All
4788 }
4789 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4790 _ => D::Next, // bare FORWARD = FORWARD 1
4791 }
4792 }
4793 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4794 self.advance();
4795 match self.peek().clone() {
4796 Token::All => {
4797 self.advance();
4798 D::BackwardAll
4799 }
4800 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4801 self.advance();
4802 D::BackwardAll
4803 }
4804 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4805 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4806 }
4807 }
4808 Token::All => {
4809 self.advance();
4810 D::All
4811 }
4812 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4813 self.advance();
4814 D::All
4815 }
4816 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4817 // Bare `FETCH <name>` — direction defaults to NEXT.
4818 _ => D::Next,
4819 };
4820 // Optional FROM / IN.
4821 if matches!(self.peek(), Token::From)
4822 || matches!(self.peek(), Token::In)
4823 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4824 {
4825 self.advance();
4826 }
4827 let name = match self.advance() {
4828 Token::Ident(n) | Token::QuotedIdent(n) => n,
4829 other => {
4830 return Err(self.err(format!("expected cursor name, got {other:?}")));
4831 }
4832 };
4833 Ok(if is_move {
4834 Statement::MoveCursor { name, direction }
4835 } else {
4836 Statement::FetchCursor { name, direction }
4837 })
4838 }
4839
4840 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4841 /// [(kind, …)] ON <col>, … FROM <table>`.
4842 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4843 self.advance(); // STATISTICS
4844 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4845 let mut if_not_exists = false;
4846 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4847 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4848 {
4849 self.advance();
4850 self.advance();
4851 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4852 self.advance();
4853 if_not_exists = true;
4854 }
4855 }
4856 let name = self.expect_ident_like()?;
4857 let mut kinds = Vec::new();
4858 if matches!(self.peek(), Token::LParen) {
4859 self.advance();
4860 loop {
4861 let k = self.expect_ident_like()?;
4862 // PG stores the single letters; accept the spelled-out
4863 // names the SQL uses and record what PG records.
4864 kinds.push(match k.to_ascii_lowercase().as_str() {
4865 "ndistinct" => String::from("d"),
4866 "dependencies" => String::from("f"),
4867 "mcv" => String::from("m"),
4868 other => {
4869 return Err(
4870 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4871 );
4872 }
4873 });
4874 match self.advance() {
4875 Token::Comma => {}
4876 Token::RParen => break,
4877 other => {
4878 return Err(self.err(alloc::format!(
4879 "expected ',' or ')' in statistics kind list, got {other:?}"
4880 )));
4881 }
4882 }
4883 }
4884 }
4885 if !matches!(self.peek(), Token::On) {
4886 return Err(self.err(alloc::format!(
4887 "expected ON in CREATE STATISTICS, got {:?}",
4888 self.peek()
4889 )));
4890 }
4891 self.advance();
4892 let mut columns = Vec::new();
4893 loop {
4894 columns.push(self.expect_ident_like()?);
4895 if matches!(self.peek(), Token::Comma) {
4896 self.advance();
4897 } else {
4898 break;
4899 }
4900 }
4901 if !matches!(self.peek(), Token::From) {
4902 return Err(self.err(alloc::format!(
4903 "expected FROM in CREATE STATISTICS, got {:?}",
4904 self.peek()
4905 )));
4906 }
4907 self.advance();
4908 let table = self.expect_ident_like()?;
4909 Ok(Statement::CreateStatistics {
4910 name,
4911 if_not_exists,
4912 kinds,
4913 columns,
4914 table,
4915 })
4916 }
4917
4918 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4919 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4920 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4921 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4922 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4923 /// forward call.
4924 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4925 self.advance(); // TABLE
4926 let if_exists = self.consume_if_exists();
4927 let mut names: Vec<String> = Vec::new();
4928 loop {
4929 names.push(self.expect_ident_like()?);
4930 if matches!(self.peek(), Token::Comma) {
4931 self.advance();
4932 continue;
4933 }
4934 break;
4935 }
4936 if matches!(
4937 self.peek(),
4938 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4939 || s.eq_ignore_ascii_case("restrict")
4940 ) {
4941 self.advance();
4942 }
4943 Ok(Statement::DropTable { names, if_exists })
4944 }
4945
4946 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4947 self.advance(); // STATISTICS
4948 let mut if_exists = false;
4949 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4950 && matches!(self.tokens.get(self.pos + 1),
4951 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4952 {
4953 self.advance();
4954 self.advance();
4955 if_exists = true;
4956 }
4957 let name = self.expect_ident_like()?;
4958 Ok(Statement::DropStatistics { name, if_exists })
4959 }
4960
4961 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4962 debug_assert!(matches!(self.peek(), Token::Create));
4963 self.advance();
4964 match self.peek() {
4965 Token::Table => self.parse_create_table_stmt_after_create(),
4966 Token::Index => self.parse_create_index_stmt_after_create(false),
4967 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4968 // object now. It used to be consumed by the CREATE-noise
4969 // arm, so a pg_dump that declares extended statistics
4970 // restored silently without them and reflection showed
4971 // nothing.
4972 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4973 self.parse_create_statistics_after_create()
4974 }
4975 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4976 // The `UNIQUE` modifier turns a partial index into a
4977 // partial-uniqueness invariant (only rows matching the
4978 // WHERE predicate are checked for duplicates). mailrs
4979 // K1 (3 hits: email_templates default, calendar_events
4980 // master, calendar_events instance).
4981 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4982 self.advance();
4983 if !matches!(self.peek(), Token::Index) {
4984 return Err(self.err(alloc::format!(
4985 "expected INDEX after CREATE UNIQUE, got {:?}",
4986 self.peek()
4987 )));
4988 }
4989 self.parse_create_index_stmt_after_create(true)
4990 }
4991 Token::Publication => {
4992 self.advance();
4993 self.parse_create_publication_after_keyword()
4994 }
4995 Token::Subscription => {
4996 self.advance();
4997 self.parse_create_subscription_after_keyword()
4998 }
4999 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
5000 // USER isn't a reserved keyword — we look for the bare
5001 // identifier so the lexer doesn't have to grow a token.
5002 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
5003 self.advance();
5004 self.parse_create_user_after_keyword(true)
5005 }
5006 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
5007 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
5008 // the default of the LOGIN attribute.
5009 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
5010 self.advance();
5011 self.parse_create_user_after_keyword(false)
5012 }
5013 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
5014 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
5015 self.advance();
5016 self.parse_create_policy_after_keyword()
5017 }
5018 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
5019 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
5020 // no-op. mailrs follow-up F3.
5021 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
5022 self.advance();
5023 self.parse_create_extension_after_keyword()
5024 }
5025 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
5026 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
5027 // optional; absorb it here and forward to the
5028 // per-kind parsers with the flag. OR is a reserved
5029 // keyword token.
5030 Token::Or => {
5031 self.advance();
5032 let next = self.peek();
5033 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
5034 return Err(self.err(alloc::format!(
5035 "expected REPLACE after CREATE OR, got {next:?}"
5036 )));
5037 };
5038 if !s2.eq_ignore_ascii_case("replace") {
5039 return Err(self.err(alloc::format!(
5040 "expected REPLACE after CREATE OR, got {s2:?}"
5041 )));
5042 }
5043 self.advance();
5044 self.parse_create_function_or_trigger_after_or_replace(true)
5045 }
5046 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
5047 self.advance();
5048 self.parse_create_function_after_keyword(false)
5049 }
5050 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
5051 self.advance();
5052 self.parse_create_trigger_after_keyword(false)
5053 }
5054 // v7.39 (round 139) — CREATE RULE …
5055 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
5056 self.advance();
5057 self.parse_create_rule_after_keyword(false)
5058 }
5059 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
5060 // trigger is a row-level AFTER trigger that additionally carries
5061 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
5062 // path already tolerates and skips those clauses, so consuming the
5063 // CONSTRAINT keyword and reusing it makes the statement parse and the
5064 // trigger fire. (The deferral timing itself is not yet honoured —
5065 // SPG fires it as a plain AFTER trigger, which is correct behaviour
5066 // for every non-deferred use.)
5067 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5068 self.advance();
5069 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5070 if t.eq_ignore_ascii_case("trigger"))
5071 {
5072 return Err(self.err(alloc::format!(
5073 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5074 self.peek()
5075 )));
5076 }
5077 self.advance();
5078 self.parse_create_trigger_after_keyword(false)
5079 }
5080 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5081 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5082 self.advance();
5083 self.parse_create_sequence_after_keyword(false)
5084 }
5085 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5086 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5087 self.advance();
5088 self.parse_create_view_after_keyword(false, false, false)
5089 }
5090 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5091 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5092 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5093 // appear (in any order) between `CREATE` and `VIEW` in
5094 // every mysqldump-emitted view. Pre-2.6 the parser
5095 // rejected the prefix and the customer's whole view
5096 // backup failed on the first view. The hints are pure
5097 // planner / permission metadata; SPG's view-rewrite
5098 // path is semantically equivalent for all three
5099 // algorithms in v7.17 (TEMPTABLE differs only in
5100 // perf for huge views — out of v7.17 scope), and
5101 // DEFINER / SQL SECURITY are pure single-user
5102 // permissioning that SPG ignores by design.
5103 Token::Ident(s) | Token::QuotedIdent(s)
5104 if s.eq_ignore_ascii_case("algorithm")
5105 || s.eq_ignore_ascii_case("definer")
5106 || s.eq_ignore_ascii_case("sql") =>
5107 {
5108 self.consume_mysql_view_prefix()?;
5109 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5110 // (in any order, in any combination), the next
5111 // keyword must be VIEW. mysqldump never emits these
5112 // prefixes on non-view statements.
5113 let next = self.peek().clone();
5114 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5115 if s2.eq_ignore_ascii_case("view"))
5116 {
5117 self.advance();
5118 self.parse_create_view_after_keyword(false, false, false)
5119 } else {
5120 Err(self.err(alloc::format!(
5121 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5122 )))
5123 }
5124 }
5125 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5126 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5127 self.advance();
5128 self.parse_create_type_after_keyword()
5129 }
5130 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5131 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5132 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5133 self.advance();
5134 self.parse_create_domain_after_keyword()
5135 }
5136 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5137 // name [AUTHORIZATION user]. Real catalog registry
5138 // (was silent-no-op'd pre-v7.17).
5139 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5140 self.advance();
5141 let if_not_exists = self.parse_if_not_exists();
5142 let name = self.expect_ident_like()?;
5143 // Optional `AUTHORIZATION <user>` trailer — accepted,
5144 // ignored (single-user catalog).
5145 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5146 if s.eq_ignore_ascii_case("authorization"))
5147 {
5148 self.advance();
5149 let _ = self.expect_ident_like()?;
5150 }
5151 Ok(Statement::CreateSchema { name, if_not_exists })
5152 }
5153 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5155 self.advance();
5156 let next = self.peek().clone();
5157 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5158 {
5159 self.advance();
5160 self.parse_create_materialized_view_after_keyword()
5161 } else {
5162 Err(self.err(alloc::format!(
5163 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5164 )))
5165 }
5166 }
5167 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5168 // no-op below), an UNLOGGED table is a real, fully-usable table in
5169 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5170 // durability optimisation is a follow-up), so a dump / app that
5171 // declares UNLOGGED tables works instead of failing to parse.
5172 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5173 self.advance(); // UNLOGGED
5174 if matches!(self.peek(), Token::Table) {
5175 self.parse_create_table_stmt_after_create()
5176 } else {
5177 Err(self.err(format!(
5178 "expected TABLE after CREATE UNLOGGED, got {:?}",
5179 self.peek()
5180 )))
5181 }
5182 }
5183 Token::Ident(s) | Token::QuotedIdent(s)
5184 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5185 {
5186 self.advance();
5187 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5188 let next = self.peek().clone();
5189 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5190 {
5191 self.advance();
5192 self.parse_create_sequence_after_keyword(true)
5193 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5194 {
5195 self.advance();
5196 self.parse_create_view_after_keyword(false, false, true)
5197 } else {
5198 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5199 // consumed and answered OK while creating nothing, so
5200 // every statement that touched the table afterwards failed
5201 // with "table not found" — the DDL itself lied. It is a
5202 // real CREATE TABLE now, marked temporary so the executor
5203 // puts it in the session's own namespace. An optional
5204 // TABLE keyword may or may not be present (`CREATE TEMP t`
5205 // is not legal, but the keyword is consumed by the
5206 // CREATE TABLE parser itself).
5207 let stmt = self.parse_create_table_stmt_after_create()?;
5208 match stmt {
5209 Statement::CreateTable(mut c) => {
5210 c.temporary = true;
5211 Ok(Statement::CreateTable(c))
5212 }
5213 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5214 // CTAS node, which needs the same session namespace.
5215 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5216 m.temporary = true;
5217 Ok(Statement::CreateMaterializedView(m))
5218 }
5219 other => Ok(other),
5220 }
5221 }
5222 }
5223 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5224 // BEGIN <body> END`. The body may reference `@var`
5225 // session variables, SET statements, internal `;`
5226 // terminators, etc. SPG has no procedure runtime, so
5227 // consume the whole `CREATE PROCEDURE … END` block as
5228 // a no-op so mysqldump scripts that include stored
5229 // routines load through. The matching-END consumer
5230 // tracks BEGIN/END nesting depth to handle nested
5231 // BEGIN blocks correctly.
5232 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5233 self.consume_mysql_routine_body();
5234 Ok(Statement::Empty)
5235 }
5236 // v7.14.0 — pg_dump / mysqldump emit
5237 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5238 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5239 // SPG is single-schema / single-database; these have
5240 // no behavioural effect, so consume + return Empty.
5241 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5242 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5243 // moved up to real parser branches. DATABASE / ROLE /
5244 // POLICY / OPERATOR stay no-op forever
5245 // (single-database, hardcoded roles).
5246 Token::Ident(s) | Token::QuotedIdent(s)
5247 if matches!(
5248 s.to_ascii_lowercase().as_str(),
5249 "database"
5250 | "role"
5251 | "operator"
5252 | "cast"
5253 | "aggregate"
5254 | "language"
5255 | "collation"
5256 | "conversion"
5257 // v7.17.0 Phase 8 (audit N6) — rarely-
5258 // emitted pg_dump shapes that should
5259 // load through without a parser error.
5260 // SPG has no planner statistics catalog,
5261 // no event-trigger hooks, no foreign-
5262 // data-wrapper infrastructure; consume
5263 // + return Empty.
5264 | "statistics"
5265 | "event"
5266 // v7.37.17 (17.6 siblings) — additional CREATE
5267 // targets pg_dump / operator install scripts
5268 // may emit that SPG has no matching machinery
5269 // for. Consume + Empty-return.
5270 | "text"
5271 | "tablespace"
5272 | "access"
5273 | "large"
5274 ) =>
5275 {
5276 // DATABASE is the one member of this list PG refuses
5277 // inside a transaction block; the rest (ROLE, CAST,
5278 // TABLESPACE, …) it runs there quite happily, so only
5279 // this one is named. Still a no-op otherwise — SPG is
5280 // single-database.
5281 let is_database = s.eq_ignore_ascii_case("database");
5282 // The name is the first token after DATABASE, past an
5283 // `IF NOT EXISTS`.
5284 let name = if is_database {
5285 self.scan_database_name()
5286 } else {
5287 None
5288 };
5289 let collation = if is_database {
5290 self.scan_database_collation_until_boundary()
5291 } else {
5292 self.consume_until_statement_boundary();
5293 None
5294 };
5295 if is_database {
5296 return Ok(Statement::NoOpPreventedInTransaction {
5297 what: String::from("CREATE DATABASE"),
5298 collation,
5299 name,
5300 });
5301 }
5302 Ok(Statement::Empty)
5303 }
5304 // v7.39 (round 706) — the foreign-data family leaves the silent
5305 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5306 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5307 // FDW machinery), but the ENGINE now warns, so a restore log
5308 // says what will not function instead of reporting success.
5309 Token::Ident(s) | Token::QuotedIdent(s)
5310 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5311 {
5312 self.consume_until_statement_boundary();
5313 Ok(Statement::ValidateOnly {
5314 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5315 names: Vec::new(),
5316 })
5317 }
5318 other => Err(self.err(format!(
5319 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5320 ))),
5321 }
5322 }
5323
5324 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5325 /// keyword decides whether we parse a function or trigger
5326 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5327 /// PROCEDURE) — those land in later releases.
5328 fn parse_create_function_or_trigger_after_or_replace(
5329 &mut self,
5330 or_replace: bool,
5331 ) -> Result<Statement, ParseError> {
5332 let tok = self.peek();
5333 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5334 return Err(self.err(alloc::format!(
5335 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5336 )));
5337 };
5338 if s.eq_ignore_ascii_case("function") {
5339 self.advance();
5340 self.parse_create_function_after_keyword(or_replace)
5341 } else if s.eq_ignore_ascii_case("trigger") {
5342 self.advance();
5343 self.parse_create_trigger_after_keyword(or_replace)
5344 } else if s.eq_ignore_ascii_case("rule") {
5345 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5346 self.advance();
5347 self.parse_create_rule_after_keyword(or_replace)
5348 } else if s.eq_ignore_ascii_case("view") {
5349 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5350 self.advance();
5351 self.parse_create_view_after_keyword(or_replace, false, false)
5352 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5353 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5354 self.advance();
5355 let nxt = self.peek().clone();
5356 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5357 {
5358 self.advance();
5359 self.parse_create_view_after_keyword(or_replace, false, true)
5360 } else {
5361 Err(self.err(alloc::format!(
5362 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5363 )))
5364 }
5365 } else {
5366 Err(self.err(alloc::format!(
5367 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5368 )))
5369 }
5370 }
5371
5372 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5373 /// SPG doesn't have a registry; pgvector / similar are
5374 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5375 /// the syntax lets dual-target schemas keep the line.
5376 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5377 // Optional `IF NOT EXISTS`.
5378 self.consume_if_not_exists();
5379 let name = self.expect_ident_like()?;
5380 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5381 // CASCADE / FROM '<v>' clauses; we don't model them.
5382 loop {
5383 match self.peek() {
5384 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5385 self.advance();
5386 continue;
5387 }
5388 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5389 self.advance();
5390 let _ = self.expect_ident_like()?;
5391 continue;
5392 }
5393 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5394 self.advance();
5395 // String or ident literal.
5396 let _ = self.advance();
5397 continue;
5398 }
5399 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5400 self.advance();
5401 let _ = self.advance();
5402 continue;
5403 }
5404 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5405 self.advance();
5406 continue;
5407 }
5408 _ => break,
5409 }
5410 }
5411 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5412 // nosuch` reported success and `pg_extension` then did not list it,
5413 // which is the accept-and-do-nothing shape F31 exists to find.
5414 Ok(Statement::ValidateOnly {
5415 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5416 names: alloc::vec![name],
5417 })
5418 }
5419
5420 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5421 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5422 /// already been consumed by the caller. Grammar accepted:
5423 ///
5424 /// name `(` arg-list `)`
5425 /// `RETURNS` return-type
5426 /// [ `LANGUAGE` ident ]
5427 /// `AS` $$ body $$
5428 /// [ `LANGUAGE` ident ]
5429 ///
5430 /// Either `LANGUAGE` position is allowed; PG accepts both.
5431 fn parse_create_function_after_keyword(
5432 &mut self,
5433 or_replace: bool,
5434 ) -> Result<Statement, ParseError> {
5435 let name = self.expect_ident_like()?;
5436 // Argument list. v7.12.4 commonly sees the empty `()`
5437 // (trigger functions); typed args parse and round-trip
5438 // but the executor only invokes nullary functions.
5439 if !matches!(self.peek(), Token::LParen) {
5440 return Err(self.err(alloc::format!(
5441 "expected '(' after function name {name:?}, got {:?}",
5442 self.peek()
5443 )));
5444 }
5445 self.advance();
5446 let args = self.parse_function_arg_list()?;
5447 // RETURNS clause.
5448 let tok = self.peek();
5449 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5450 return Err(self.err(alloc::format!(
5451 "expected RETURNS after function arg list, got {tok:?}"
5452 )));
5453 };
5454 if !s.eq_ignore_ascii_case("returns") {
5455 return Err(self.err(alloc::format!(
5456 "expected RETURNS after function arg list, got {s:?}"
5457 )));
5458 }
5459 self.advance();
5460 let returns = self.parse_function_return()?;
5461 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5462 // re-check after the body too).
5463 let mut language: Option<String> = self.parse_optional_language()?;
5464 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5465 // either side of the body and in any order, interleaved with
5466 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5467 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5468 // PG's own pg_dump output did not restore.
5469 let mut attrs = FunctionAttrs::default();
5470 loop {
5471 let before = self.pos;
5472 self.parse_function_attrs_into(&mut attrs)?;
5473 if language.is_none() {
5474 language = self.parse_optional_language()?;
5475 }
5476 if self.pos == before {
5477 break;
5478 }
5479 }
5480 // `AS` followed by a $$-quoted body (lexer already
5481 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5482 // Token::String). AS is a reserved keyword (Token::As).
5483 if !matches!(self.peek(), Token::As) {
5484 return Err(self.err(alloc::format!(
5485 "expected AS before function body, got {:?}",
5486 self.peek()
5487 )));
5488 }
5489 self.advance();
5490 let body_text = match self.peek() {
5491 Token::String(s) => {
5492 let body = s.clone();
5493 self.advance();
5494 body
5495 }
5496 other => {
5497 return Err(self.err(alloc::format!(
5498 "expected $$-quoted function body after AS, got {other:?}"
5499 )));
5500 }
5501 };
5502 // Trailing clauses — PG's other accepted position for both the
5503 // LANGUAGE and the attributes.
5504 loop {
5505 let before = self.pos;
5506 self.parse_function_attrs_into(&mut attrs)?;
5507 if language.is_none() {
5508 language = self.parse_optional_language()?;
5509 }
5510 if self.pos == before {
5511 break;
5512 }
5513 }
5514 let language = language.unwrap_or_else(|| String::from("sql"));
5515 // PL/pgSQL bodies get structure-parsed. Other languages
5516 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5517 // recognise) round-trip as Raw text — the executor errors
5518 // when invoked with a clear unsupported message.
5519 let body = if language.eq_ignore_ascii_case("plpgsql") {
5520 match parse_plpgsql_body(&body_text) {
5521 Ok(block) => FunctionBody::PlPgSql(block),
5522 // Best-effort: if the body parser doesn't yet
5523 // support a construct used inside, fall back to
5524 // raw — keeps `CREATE FUNCTION` itself working
5525 // (catalogue accepts), executor errors on
5526 // invocation only.
5527 Err(_) => FunctionBody::Raw(body_text),
5528 }
5529 } else {
5530 FunctionBody::Raw(body_text)
5531 };
5532 Ok(Statement::CreateFunction(CreateFunctionStatement {
5533 name,
5534 or_replace,
5535 args,
5536 returns,
5537 language,
5538 body,
5539 attrs,
5540 }))
5541 }
5542
5543 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5544 /// attribute clauses into `attrs`, stopping at the first token that
5545 /// is not one. Measured against PG 18.4, which accepts them in any
5546 /// order and on either side of the body.
5547 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5548 loop {
5549 let word = match self.peek() {
5550 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5551 // NOT LEAKPROOF — NOT is a reserved keyword token.
5552 Token::Not
5553 if matches!(
5554 self.tokens.get(self.pos + 1),
5555 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5556 ) =>
5557 {
5558 self.advance();
5559 self.advance();
5560 attrs.leakproof = false;
5561 continue;
5562 }
5563 _ => return Ok(()),
5564 };
5565 match word.as_str() {
5566 "immutable" => {
5567 self.advance();
5568 attrs.volatility = FunctionVolatility::Immutable;
5569 }
5570 "stable" => {
5571 self.advance();
5572 attrs.volatility = FunctionVolatility::Stable;
5573 }
5574 "volatile" => {
5575 self.advance();
5576 attrs.volatility = FunctionVolatility::Volatile;
5577 }
5578 "strict" => {
5579 self.advance();
5580 attrs.strict = true;
5581 }
5582 "leakproof" => {
5583 self.advance();
5584 attrs.leakproof = true;
5585 }
5586 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5587 // spelled-out forms of STRICT and its opposite.
5588 "returns" | "called" => {
5589 let strict = word == "returns";
5590 let mut probe = self.pos + 1;
5591 if strict {
5592 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5593 // is not ours.
5594 match self.tokens.get(probe) {
5595 Some(Token::Null) => probe += 1,
5596 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5597 _ => return Ok(()),
5598 }
5599 }
5600 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5601 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5602 if !ok {
5603 return Ok(());
5604 }
5605 probe += 1;
5606 match self.tokens.get(probe) {
5607 Some(Token::Null) => probe += 1,
5608 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5609 _ => return Ok(()),
5610 }
5611 match self.tokens.get(probe) {
5612 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5613 _ => return Ok(()),
5614 }
5615 self.pos = probe;
5616 attrs.strict = strict;
5617 }
5618 "security" | "external" => {
5619 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5620 let mut probe = self.pos + 1;
5621 if word == "external" {
5622 match self.tokens.get(probe) {
5623 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5624 probe += 1;
5625 }
5626 _ => return Ok(()),
5627 }
5628 }
5629 let definer = match self.tokens.get(probe) {
5630 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5631 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5632 _ => return Ok(()),
5633 };
5634 self.pos = probe + 1;
5635 attrs.security_definer = definer;
5636 }
5637 "parallel" => {
5638 let level = match self.tokens.get(self.pos + 1) {
5639 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5640 FunctionParallel::Safe
5641 }
5642 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5643 FunctionParallel::Restricted
5644 }
5645 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5646 FunctionParallel::Unsafe
5647 }
5648 _ => return Ok(()),
5649 };
5650 self.pos += 2;
5651 attrs.parallel = level;
5652 }
5653 "cost" | "rows" => {
5654 let Some(n) = self.peek_number_at(self.pos + 1) else {
5655 return Ok(());
5656 };
5657 self.pos += 2;
5658 if word == "cost" {
5659 attrs.cost = Some(n);
5660 } else {
5661 attrs.rows = Some(n);
5662 }
5663 }
5664 _ => return Ok(()),
5665 }
5666 }
5667 }
5668
5669 /// The numeric literal at `idx`, if there is one.
5670 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5671 match self.tokens.get(idx)? {
5672 Token::Integer(n) => Some(*n as f64),
5673 Token::Float(f) => Some(*f),
5674 Token::Numeric(t) => t.parse::<f64>().ok(),
5675 _ => None,
5676 }
5677 }
5678
5679 /// Closing `)`-terminated argument list. v7.12.4 commonly
5680 /// sees the empty `()`; typed args round-trip but the
5681 /// executor (yet) doesn't invoke them.
5682 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5683 /// it away, which is what PG does with one on a function parameter.
5684 fn skip_type_modifier(&mut self) {
5685 if !matches!(self.peek(), Token::LParen) {
5686 return;
5687 }
5688 // Only a numeric modifier — anything else is not one, and eating
5689 // it would swallow real grammar.
5690 let mut i = self.pos + 1;
5691 let mut seen_number = false;
5692 loop {
5693 match self.tokens.get(i) {
5694 Some(Token::Integer(_)) => seen_number = true,
5695 Some(Token::Comma) => {}
5696 Some(Token::RParen) => break,
5697 _ => return,
5698 }
5699 i += 1;
5700 }
5701 if !seen_number {
5702 return;
5703 }
5704 while self.pos <= i {
5705 self.advance();
5706 }
5707 }
5708
5709 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5710 let mut args: Vec<FunctionArg> = Vec::new();
5711 if matches!(self.peek(), Token::RParen) {
5712 self.advance();
5713 return Ok(args);
5714 }
5715 loop {
5716 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5717 // a reserved token; OUT / INOUT are bare idents.
5718 let mode = if matches!(self.peek(), Token::In) {
5719 self.advance();
5720 FunctionArgMode::In
5721 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5722 {
5723 self.advance();
5724 FunctionArgMode::Out
5725 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5726 {
5727 self.advance();
5728 FunctionArgMode::InOut
5729 } else {
5730 FunctionArgMode::In
5731 };
5732 // Optional name. The next token is either a name
5733 // (followed by a type ident) or the type itself.
5734 // Disambiguate by peeking ahead: if the token after
5735 // the next ident is also an ident, we treat the
5736 // first as the name.
5737 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5738 // the comma or paren, then decide. Reading at most two of
5739 // them could not spell `x double precision` at all, and
5740 // silently mis-read the bare `double precision` as a
5741 // parameter named "double" — which is what made the same
5742 // signature key two different ways.
5743 let (name, ty_token) = {
5744 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5745 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5746 words.push(self.expect_ident_like()?);
5747 }
5748 // v7.39 (round 344) — a length / precision modifier on the
5749 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5750 // accepts it and DROPS it — `pg_get_function_arguments`
5751 // reports plain `character varying` / `numeric`, measured on
5752 // 18.4 — but SPG raised `syntax error at or near "("`,
5753 // because the modifier's parens were never consumed.
5754 self.skip_type_modifier();
5755 // r1049 — `f(v bigint[])`. The array suffix parsed in
5756 // the column position, the cast position and (r1038)
5757 // the RETURNS position, but not here: the fifth
5758 // member of the same family, reported by sentori as
5759 // presumably the same code. It is now.
5760 let array_suffix = self.consume_array_suffix();
5761 let whole = words.join(" ");
5762 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5763 {
5764 (Some(words[0].clone()), words[1..].join(" "))
5765 } else {
5766 (None, whole)
5767 };
5768 ty_token.push_str(&array_suffix);
5769 (name, ty_token)
5770 };
5771 // Type — try to map to ColumnTypeName, else Raw.
5772 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5773 Some(t) => FunctionArgType::Typed(t),
5774 None => FunctionArgType::Raw(ty_token),
5775 };
5776 args.push(FunctionArg { mode, name, ty });
5777 match self.peek() {
5778 Token::Comma => {
5779 self.advance();
5780 continue;
5781 }
5782 Token::RParen => {
5783 self.advance();
5784 return Ok(args);
5785 }
5786 other => {
5787 return Err(self.err(alloc::format!(
5788 "expected , or ) in function arg list, got {other:?}"
5789 )));
5790 }
5791 }
5792 }
5793 }
5794
5795 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5796 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5797 // function whose row shape is named inline.
5798 if matches!(self.peek(), Token::Table)
5799 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5800 {
5801 self.advance(); // TABLE
5802 self.advance(); // (
5803 let mut cols: Vec<String> = Vec::new();
5804 loop {
5805 let cname = self.expect_ident_like()?;
5806 let mut ty: Vec<String> = Vec::new();
5807 loop {
5808 match self.peek() {
5809 Token::Comma | Token::RParen | Token::Eof => break,
5810 _ => {}
5811 }
5812 match self.advance() {
5813 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5814 other => {
5815 if let Some(w) = unreserved_keyword_text(&other) {
5816 ty.push(w);
5817 }
5818 }
5819 }
5820 }
5821 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5822 if matches!(self.peek(), Token::Comma) {
5823 self.advance();
5824 } else {
5825 break;
5826 }
5827 }
5828 if matches!(self.peek(), Token::RParen) {
5829 self.advance();
5830 }
5831 return Ok(FunctionReturn::Other(alloc::format!(
5832 "TABLE({})",
5833 cols.join(", ")
5834 )));
5835 }
5836 let ident = self.expect_ident_like()?;
5837 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5838 if ident.eq_ignore_ascii_case("setof") {
5839 let inner = self.expect_ident_like()?;
5840 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5841 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5842 }
5843 if ident.eq_ignore_ascii_case("trigger") {
5844 return Ok(FunctionReturn::Trigger);
5845 }
5846 if ident.eq_ignore_ascii_case("void") {
5847 return Ok(FunctionReturn::Void);
5848 }
5849 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5850 // RETURN position did not, so the `[` was a syntax error and the
5851 // whole migration stopped. sentori worked around it by returning
5852 // zero-padded text.
5853 let suffix = self.consume_array_suffix();
5854 if !suffix.is_empty() {
5855 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5856 }
5857 match map_type_ident_to_column_type_name(&ident) {
5858 Some(t) => Ok(FunctionReturn::Type(t)),
5859 None => Ok(FunctionReturn::Other(ident)),
5860 }
5861 }
5862
5863 /// Consume any `[]` / `[N]` array markers after a type name and give
5864 /// back their text. Empty when there are none.
5865 fn consume_array_suffix(&mut self) -> String {
5866 let mut out = String::new();
5867 while matches!(self.peek(), Token::LBracket) {
5868 self.advance();
5869 // `[N]` is accepted and, as in PG, the length is not enforced.
5870 if let Token::Integer(n) = self.peek().clone() {
5871 self.advance();
5872 out.push_str(&alloc::format!("[{n}]"));
5873 } else {
5874 out.push_str("[]");
5875 }
5876 if matches!(self.peek(), Token::RBracket) {
5877 self.advance();
5878 }
5879 }
5880 out
5881 }
5882
5883 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5884 match self.peek() {
5885 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5886 self.advance();
5887 let lang = self.expect_ident_like()?;
5888 Ok(Some(lang.to_ascii_lowercase()))
5889 }
5890 _ => Ok(None),
5891 }
5892 }
5893
5894 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5895 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5896 /// (expr)]*`. The `DOMAIN` keyword has already been
5897 /// consumed. PG allows the trailing constraints in any
5898 /// order; we approximate with a small loop.
5899 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5900 let name = self.expect_ident_like()?;
5901 // Optional `AS`.
5902 if matches!(self.peek(), Token::As) {
5903 self.advance();
5904 }
5905 // v7.39 (round 259) — keep the raw type NAME when the base is not
5906 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5907 // parent domain.
5908 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5909 self.parse_type_with_implied_flags()?;
5910 let mut default: Option<Expr> = None;
5911 let mut not_null = false;
5912 let mut checks: Vec<Expr> = Vec::new();
5913 loop {
5914 match self.peek() {
5915 Token::Default => {
5916 if default.is_some() {
5917 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5918 }
5919 self.advance();
5920 default = Some(self.parse_expr(0)?);
5921 }
5922 Token::Not => {
5923 self.advance();
5924 if !matches!(self.peek(), Token::Null) {
5925 return Err(self.err(alloc::format!(
5926 "expected NULL after NOT in DOMAIN, got {:?}",
5927 self.peek()
5928 )));
5929 }
5930 self.advance();
5931 not_null = true;
5932 }
5933 Token::Null => {
5934 self.advance();
5935 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5936 // is the default-nullable marker (PG accepts it),
5937 // but AFTER a NOT NULL it is a conflict PG refuses
5938 // (`conflicting NULL/NOT NULL constraints`,
5939 // PG18-measured); the old arm no-opped both ways.
5940 if not_null {
5941 return Err(self.err(alloc::string::String::from(
5942 "conflicting NULL/NOT NULL constraints",
5943 )));
5944 }
5945 }
5946 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5947 self.advance();
5948 if !matches!(self.peek(), Token::LParen) {
5949 return Err(self.err(alloc::format!(
5950 "expected '(' after CHECK in DOMAIN, got {:?}",
5951 self.peek()
5952 )));
5953 }
5954 self.advance();
5955 let expr = self.parse_expr(0)?;
5956 if !matches!(self.peek(), Token::RParen) {
5957 return Err(self.err(alloc::format!(
5958 "expected ')' after CHECK expr, got {:?}",
5959 self.peek()
5960 )));
5961 }
5962 self.advance();
5963 checks.push(expr);
5964 }
5965 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5966 // prefix on the constraint; we drop the name and
5967 // recurse into the constraint parsing.
5968 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5969 self.advance();
5970 let _ = self.expect_ident_like()?;
5971 }
5972 _ => break,
5973 }
5974 }
5975 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5976 name,
5977 base_type,
5978 base_domain: base_user_ref,
5979 default,
5980 not_null,
5981 checks,
5982 }))
5983 }
5984
5985 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5986 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5987 /// consumed.
5988 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5989 let name = self.expect_ident_like()?;
5990 // Required `AS`.
5991 if !matches!(self.peek(), Token::As) {
5992 return Err(self.err(alloc::format!(
5993 "expected AS after CREATE TYPE {name:?}, got {:?}",
5994 self.peek()
5995 )));
5996 }
5997 self.advance();
5998 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5999 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
6000 // on the next token: `(` = composite, ident `ENUM` = enum.
6001 if matches!(self.peek(), Token::LParen) {
6002 self.advance();
6003 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
6004 let mut field_user_types: Vec<Option<String>> = Vec::new();
6005 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
6006 // is legal PG (an attribute-less composite; measured — the old
6007 // e2e note claimed PG requires at least one attribute).
6008 if matches!(self.peek(), Token::RParen) {
6009 self.advance();
6010 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6011 name,
6012 kind: crate::ast::TypeKind::Composite {
6013 fields,
6014 field_user_types,
6015 },
6016 }));
6017 }
6018 loop {
6019 let field_name = self.expect_ident_like()?;
6020 // v7.39 (round 264) — keep the raw type name when it is not
6021 // a builtin: that is how a NESTED composite field records
6022 // which composite it holds.
6023 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
6024 self.parse_type_with_implied_flags()?;
6025 fields.push((field_name, field_type));
6026 field_user_types.push(field_user_ref);
6027 if matches!(self.peek(), Token::Comma) {
6028 self.advance();
6029 continue;
6030 }
6031 if matches!(self.peek(), Token::RParen) {
6032 self.advance();
6033 break;
6034 }
6035 return Err(self.err(alloc::format!(
6036 "expected , or ) in composite field list, got {:?}",
6037 self.peek()
6038 )));
6039 }
6040 if fields.is_empty() {
6041 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
6042 }
6043 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6044 name,
6045 kind: crate::ast::TypeKind::Composite {
6046 fields,
6047 field_user_types,
6048 },
6049 }));
6050 }
6051 // Required `ENUM` ident.
6052 let kind_ident = match self.peek().clone() {
6053 Token::Ident(s) | Token::QuotedIdent(s) => s,
6054 other => {
6055 return Err(self.err(alloc::format!(
6056 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
6057 )));
6058 }
6059 };
6060 if !kind_ident.eq_ignore_ascii_case("enum") {
6061 return Err(self.err(alloc::format!(
6062 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
6063 )));
6064 }
6065 self.advance();
6066 if !matches!(self.peek(), Token::LParen) {
6067 return Err(self.err(alloc::format!(
6068 "expected '(' after ENUM, got {:?}",
6069 self.peek()
6070 )));
6071 }
6072 self.advance();
6073 let mut labels: Vec<String> = Vec::new();
6074 loop {
6075 match self.peek().clone() {
6076 Token::String(s) => {
6077 self.advance();
6078 labels.push(s);
6079 }
6080 other => {
6081 return Err(
6082 self.err(alloc::format!("expected enum label string, got {other:?}"))
6083 );
6084 }
6085 }
6086 if matches!(self.peek(), Token::Comma) {
6087 self.advance();
6088 continue;
6089 }
6090 if matches!(self.peek(), Token::RParen) {
6091 self.advance();
6092 break;
6093 }
6094 return Err(self.err(alloc::format!(
6095 "expected , or ) in ENUM label list, got {:?}",
6096 self.peek()
6097 )));
6098 }
6099 if labels.is_empty() {
6100 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6101 }
6102 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6103 name,
6104 kind: crate::ast::TypeKind::Enum { labels },
6105 }))
6106 }
6107
6108 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6109 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6110 /// The `CREATE MATERIALIZED VIEW` keywords have already been
6111 /// consumed.
6112 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6113 let if_not_exists = self.parse_if_not_exists();
6114 let name = self.expect_ident_like()?;
6115 let mut columns: Vec<String> = Vec::new();
6116 if matches!(self.peek(), Token::LParen) {
6117 self.advance();
6118 loop {
6119 let c = self.expect_ident_like()?;
6120 columns.push(c);
6121 if matches!(self.peek(), Token::Comma) {
6122 self.advance();
6123 continue;
6124 }
6125 if matches!(self.peek(), Token::RParen) {
6126 self.advance();
6127 break;
6128 }
6129 return Err(self.err(alloc::format!(
6130 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6131 self.peek()
6132 )));
6133 }
6134 }
6135 if !matches!(self.peek(), Token::As) {
6136 return Err(self.err(alloc::format!(
6137 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6138 self.peek()
6139 )));
6140 }
6141 self.advance();
6142 // v7.39 (round 151) — a WITH-headed body is legal (read-only
6143 // CTEs only; the engine rejects data-modifying ones with PG's
6144 // message). A trailing `WITH [NO] DATA` can't START the body,
6145 // so WITH here heads the query.
6146 let body = if self.peek_is_with_kw() {
6147 self.advance();
6148 self.parse_nested_with_select()?
6149 } else {
6150 let body_stmt = self.parse_select_stmt()?;
6151 let Statement::Select(body) = body_stmt else {
6152 return Err(self.err(alloc::format!(
6153 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6154 )));
6155 };
6156 body
6157 };
6158 // Optional trailing `WITH [NO] DATA`.
6159 let with_data = self.parse_optional_with_data(true)?;
6160 Ok(Statement::CreateMaterializedView(
6161 crate::ast::CreateMaterializedViewStatement {
6162 temporary: false,
6163 name,
6164 if_not_exists,
6165 columns,
6166 body,
6167 with_data,
6168 as_plain_table: false,
6169 },
6170 ))
6171 }
6172
6173 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6174 /// `default_when_absent` is what to return if the tail is
6175 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6176 /// WITH DATA).
6177 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6178 let save = self.pos;
6179 // `WITH` is an Ident (not reserved in the lexer).
6180 let is_with = match self.peek() {
6181 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6182 _ => false,
6183 };
6184 if !is_with {
6185 return Ok(default_when_absent);
6186 }
6187 self.advance();
6188 // Optional `NO`.
6189 let mut with_data = true;
6190 let is_no = match self.peek() {
6191 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6192 _ => false,
6193 };
6194 if is_no {
6195 self.advance();
6196 with_data = false;
6197 }
6198 // Required `DATA` ident.
6199 let is_data = match self.peek() {
6200 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6201 _ => false,
6202 };
6203 if is_data {
6204 self.advance();
6205 Ok(with_data)
6206 } else {
6207 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6208 // parser can interpret it.
6209 self.pos = save;
6210 Ok(default_when_absent)
6211 }
6212 }
6213
6214 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6215 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6216 /// All keyword prefixes have already been consumed; the flags
6217 /// say which were present.
6218 fn parse_create_view_after_keyword(
6219 &mut self,
6220 or_replace: bool,
6221 _materialized_unused: bool,
6222 temporary: bool,
6223 ) -> Result<Statement, ParseError> {
6224 let if_not_exists = self.parse_if_not_exists();
6225 let name = self.expect_ident_like()?;
6226 // Optional `(col, col, …)` rename list.
6227 let mut columns: Vec<String> = Vec::new();
6228 if matches!(self.peek(), Token::LParen) {
6229 self.advance();
6230 loop {
6231 let c = self.expect_ident_like()?;
6232 columns.push(c);
6233 if matches!(self.peek(), Token::Comma) {
6234 self.advance();
6235 continue;
6236 }
6237 if matches!(self.peek(), Token::RParen) {
6238 self.advance();
6239 break;
6240 }
6241 return Err(self.err(alloc::format!(
6242 "expected , or ) in VIEW column list, got {:?}",
6243 self.peek()
6244 )));
6245 }
6246 }
6247 // Required `AS`.
6248 if !matches!(self.peek(), Token::As) {
6249 return Err(self.err(alloc::format!(
6250 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6251 self.peek()
6252 )));
6253 }
6254 self.advance();
6255 // Body: a regular SELECT statement. v7.39 (round 151) — a
6256 // WITH-headed body is legal too (read-only CTEs only; the
6257 // engine rejects data-modifying ones with PG's message).
6258 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6259 // with the check-option clause, so WITH here heads the query.
6260 let body = if self.peek_is_with_kw() {
6261 self.advance();
6262 self.parse_nested_with_select()?
6263 } else {
6264 let body_stmt = self.parse_select_stmt()?;
6265 let Statement::Select(body) = body_stmt else {
6266 return Err(self.err(alloc::format!(
6267 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6268 )));
6269 };
6270 body
6271 };
6272 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6273 // The SELECT parser stops before a trailing WITH, so it lands here.
6274 let check_option = if matches!(self.peek(),
6275 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6276 {
6277 self.advance(); // WITH
6278 let opt = match self.peek() {
6279 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6280 self.advance();
6281 crate::ast::ViewCheckOption::Local
6282 }
6283 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6284 self.advance();
6285 crate::ast::ViewCheckOption::Cascaded
6286 }
6287 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6288 _ => crate::ast::ViewCheckOption::Cascaded,
6289 };
6290 if !matches!(self.peek(),
6291 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6292 {
6293 return Err(self.err(alloc::format!(
6294 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6295 self.peek()
6296 )));
6297 }
6298 self.advance(); // CHECK
6299 if !matches!(self.peek(),
6300 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6301 {
6302 return Err(self.err(alloc::format!(
6303 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6304 self.peek()
6305 )));
6306 }
6307 self.advance(); // OPTION
6308 Some(opt)
6309 } else {
6310 None
6311 };
6312 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6313 name,
6314 or_replace,
6315 if_not_exists,
6316 temporary,
6317 columns,
6318 body,
6319 check_option,
6320 }))
6321 }
6322
6323 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6324 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6325 /// consumed; `temporary` carries whether TEMPORARY was seen.
6326 fn parse_create_sequence_after_keyword(
6327 &mut self,
6328 temporary: bool,
6329 ) -> Result<Statement, ParseError> {
6330 let if_not_exists = self.parse_if_not_exists();
6331 let name = self.expect_ident_like()?;
6332 // Optional `AS data_type`.
6333 let data_type = if matches!(self.peek(), Token::As) {
6334 self.advance();
6335 Some(self.parse_sequence_data_type()?)
6336 } else {
6337 None
6338 };
6339 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6340 Ok(Statement::CreateSequence(
6341 crate::ast::CreateSequenceStatement {
6342 name,
6343 if_not_exists,
6344 temporary,
6345 data_type,
6346 options,
6347 },
6348 ))
6349 }
6350
6351 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6352 /// already been consumed; this is reached after `SEQUENCE`.
6353 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6354 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6355 use crate::ast::AlterDomainAction as A;
6356 let name = self.expect_ident_like()?;
6357 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6358 let kw = match self.peek() {
6359 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6360 Token::Drop => alloc::string::String::from("drop"),
6361 Token::Default => alloc::string::String::from("default"),
6362 other => {
6363 return Err(self.err(alloc::format!(
6364 "expected an ALTER DOMAIN action, got {other:?}"
6365 )));
6366 }
6367 };
6368 let action = match kw.as_str() {
6369 "add" => {
6370 self.advance();
6371 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6372 {
6373 self.advance();
6374 Some(self.expect_ident_like()?)
6375 } else {
6376 None
6377 };
6378 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6379 return Err(self.err(alloc::format!(
6380 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6381 self.peek()
6382 )));
6383 }
6384 self.advance();
6385 if !matches!(self.peek(), Token::LParen) {
6386 return Err(self.err("expected '(' after CHECK".into()));
6387 }
6388 self.advance();
6389 let check = self.parse_expr(0)?;
6390 if !matches!(self.peek(), Token::RParen) {
6391 return Err(self.err("expected ')' after CHECK expression".into()));
6392 }
6393 self.advance();
6394 A::AddConstraint { name: cname, check }
6395 }
6396 "drop" => {
6397 self.advance();
6398 match self.peek() {
6399 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6400 self.advance();
6401 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6402 {
6403 self.advance();
6404 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6405 {
6406 return Err(self.err("expected EXISTS after IF".into()));
6407 }
6408 self.advance();
6409 true
6410 } else {
6411 false
6412 };
6413 let cn = self.expect_ident_like()?;
6414 A::DropConstraint {
6415 name: cn,
6416 if_exists,
6417 }
6418 }
6419 Token::Default => {
6420 self.advance();
6421 A::DropDefault
6422 }
6423 Token::Not => {
6424 self.advance();
6425 if !matches!(self.peek(), Token::Null) {
6426 return Err(self.err("expected NULL after NOT".into()));
6427 }
6428 self.advance();
6429 A::DropNotNull
6430 }
6431 other => {
6432 return Err(self.err(alloc::format!(
6433 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6434 )));
6435 }
6436 }
6437 }
6438 "set" => {
6439 self.advance();
6440 match self.peek() {
6441 Token::Default => {
6442 self.advance();
6443 A::SetDefault(self.parse_expr(0)?)
6444 }
6445 Token::Not => {
6446 self.advance();
6447 if !matches!(self.peek(), Token::Null) {
6448 return Err(self.err("expected NULL after NOT".into()));
6449 }
6450 self.advance();
6451 A::SetNotNull
6452 }
6453 other => {
6454 return Err(self.err(alloc::format!(
6455 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6456 )));
6457 }
6458 }
6459 }
6460 "rename" => {
6461 self.advance();
6462 if !matches!(self.peek(), Token::To) {
6463 return Err(self.err("expected TO after RENAME".into()));
6464 }
6465 self.advance();
6466 A::RenameTo(self.expect_ident_like()?)
6467 }
6468 other => {
6469 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6470 }
6471 };
6472 Ok(Statement::AlterDomain { name, action })
6473 }
6474
6475 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6476 let if_exists = self.parse_if_exists();
6477 let name = self.expect_ident_like()?;
6478 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6479 // the option list (PG allows only one or the other).
6480 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6481 self.advance();
6482 if matches!(self.peek(), Token::To) {
6483 self.advance();
6484 } else {
6485 self.expect_keyword_ident("to")?;
6486 }
6487 let new = self.expect_ident_like()?;
6488 return Ok(Statement::AlterSequence(
6489 crate::ast::AlterSequenceStatement {
6490 name,
6491 if_exists,
6492 options: crate::ast::SequenceOptions::default(),
6493 rename_to: Some(new),
6494 },
6495 ));
6496 }
6497 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6498 Ok(Statement::AlterSequence(
6499 crate::ast::AlterSequenceStatement {
6500 name,
6501 if_exists,
6502 options,
6503 rename_to: None,
6504 },
6505 ))
6506 }
6507
6508 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6509 let kw = self.expect_ident_like()?;
6510 match kw.to_ascii_lowercase().as_str() {
6511 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6512 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6513 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6514 other => Err(self.err(alloc::format!(
6515 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6516 ))),
6517 }
6518 }
6519
6520 fn parse_sequence_options(
6521 &mut self,
6522 allow_restart: bool,
6523 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6524 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6525 let mut opts = SequenceOptions::default();
6526 #[allow(clippy::while_let_loop)]
6527 loop {
6528 // Match an ident; stop at any non-ident token (sentinel,
6529 // semicolon, end of statement).
6530 let kw_lc = match self.peek() {
6531 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6532 _ => break,
6533 };
6534 match kw_lc.as_str() {
6535 "increment" => {
6536 self.advance();
6537 // Optional BY.
6538 if self.peek_is_by() {
6539 self.advance();
6540 }
6541 opts.increment = Some(self.expect_signed_int()?);
6542 }
6543 "minvalue" => {
6544 self.advance();
6545 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6546 }
6547 "maxvalue" => {
6548 self.advance();
6549 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6550 }
6551 "no" => {
6552 self.advance();
6553 let what = self.expect_ident_like()?;
6554 match what.to_ascii_lowercase().as_str() {
6555 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6556 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6557 "cycle" => opts.cycle = Some(false),
6558 other => {
6559 return Err(self.err(alloc::format!(
6560 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6561 )));
6562 }
6563 }
6564 }
6565 "start" => {
6566 self.advance();
6567 // Optional WITH.
6568 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6569 if s.eq_ignore_ascii_case("with"))
6570 {
6571 self.advance();
6572 }
6573 opts.start = Some(self.expect_signed_int()?);
6574 }
6575 "restart" if allow_restart => {
6576 self.advance();
6577 // Optional WITH n; bare RESTART means restart at START.
6578 let mut with_val: Option<i64> = None;
6579 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6580 if s.eq_ignore_ascii_case("with"))
6581 {
6582 self.advance();
6583 with_val = Some(self.expect_signed_int()?);
6584 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6585 with_val = Some(self.expect_signed_int()?);
6586 }
6587 opts.restart = Some(with_val);
6588 }
6589 "cache" => {
6590 self.advance();
6591 opts.cache = Some(self.expect_signed_int()?);
6592 }
6593 "cycle" => {
6594 self.advance();
6595 opts.cycle = Some(true);
6596 }
6597 "owned" => {
6598 self.advance();
6599 match self.peek() {
6600 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6601 self.advance();
6602 }
6603 other => {
6604 return Err(
6605 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6606 );
6607 }
6608 }
6609 // OWNED BY {NONE | tab.col}. Read just one ident
6610 // (NOT expect_ident_like which would auto-strip
6611 // a schema prefix and consume the `.col` we need).
6612 let first = match self.advance() {
6613 Token::Ident(s) | Token::QuotedIdent(s) => s,
6614 other => {
6615 return Err(self.err(alloc::format!(
6616 "expected identifier or NONE after OWNED BY, got {other:?}"
6617 )));
6618 }
6619 };
6620 if first.eq_ignore_ascii_case("none") {
6621 opts.owned_by = Some(SequenceOwnedBy::None);
6622 } else if matches!(self.peek(), Token::Dot) {
6623 self.advance();
6624 let second = match self.advance() {
6625 Token::Ident(s) | Token::QuotedIdent(s) => s,
6626 other => {
6627 return Err(self.err(alloc::format!(
6628 "expected column name after OWNED BY {first}., got {other:?}"
6629 )));
6630 }
6631 };
6632 // v7.17 dump-compat fix — pg_dump emits
6633 // OWNED BY clauses as
6634 // `schema.table.column` (three segments).
6635 // If a third `.<ident>` follows, treat the
6636 // first ident as schema (drop it; SPG is
6637 // single-schema) and the middle / last
6638 // pair as table.column. Otherwise it's
6639 // the two-segment form table.column.
6640 if matches!(self.peek(), Token::Dot) {
6641 self.advance();
6642 let third = match self.advance() {
6643 Token::Ident(s) | Token::QuotedIdent(s) => s,
6644 other => {
6645 return Err(self.err(alloc::format!(
6646 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6647 )));
6648 }
6649 };
6650 let _ = first; // schema prefix discarded
6651 opts.owned_by = Some(SequenceOwnedBy::Column {
6652 table: second,
6653 column: third,
6654 });
6655 } else {
6656 opts.owned_by = Some(SequenceOwnedBy::Column {
6657 table: first,
6658 column: second,
6659 });
6660 }
6661 } else {
6662 return Err(self.err(alloc::format!(
6663 "expected table.column or NONE after OWNED BY, got {first:?}"
6664 )));
6665 }
6666 }
6667 _ => break,
6668 }
6669 }
6670 Ok(opts)
6671 }
6672
6673 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6674 let neg = if matches!(self.peek(), Token::Minus) {
6675 self.advance();
6676 true
6677 } else {
6678 false
6679 };
6680 match self.peek() {
6681 Token::Integer(n) => {
6682 let v = *n;
6683 self.advance();
6684 Ok(if neg { -v } else { v })
6685 }
6686 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6687 }
6688 }
6689
6690 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6691 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6692 /// clause is fully accepted and discarded — SPG always runs
6693 /// constraint checks immediately (single-writer model). The
6694 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6695 /// in either order (per the SQL spec they're independent),
6696 /// though pg_dump always emits them in the canonical
6697 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6698 /// Stops at the first token that isn't part of the clause.
6699 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6700 self.consume_deferrable_clauses_timed().map(|_| ())
6701 }
6702
6703 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6704 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6705 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6706 /// NOT DEFERRABLE and a circular-FK migration could not load.
6707 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6708 let mut deferrable = false;
6709 let mut initially_deferred = false;
6710 loop {
6711 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6712 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6713 self.advance();
6714 deferrable = true;
6715 if self.consume_optional_initially_clause()? {
6716 initially_deferred = true;
6717 }
6718 continue;
6719 }
6720 // `NOT DEFERRABLE` — already worked pre-3.1.
6721 if matches!(self.peek(), Token::Not) {
6722 let look = self.tokens.get(self.pos + 1);
6723 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6724 self.advance(); // NOT
6725 self.advance(); // DEFERRABLE
6726 deferrable = false;
6727 initially_deferred = false;
6728 let _ = self.consume_optional_initially_clause()?;
6729 continue;
6730 }
6731 break;
6732 }
6733 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6734 // accepts this without a leading [NOT] DEFERRABLE
6735 // (the timing keyword alone). pg_dump occasionally
6736 // emits it on FK constraints that inherit timing.
6737 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6738 if self.consume_optional_initially_clause()? {
6739 initially_deferred = true;
6740 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6741 deferrable = true;
6742 }
6743 continue;
6744 }
6745 break;
6746 }
6747 Ok((deferrable, initially_deferred))
6748 }
6749
6750 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6751 /// next token is `INITIALLY`, consume it plus the required
6752 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6753 /// Returns true when the timing seen was `DEFERRED`.
6754 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6755 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6756 return Ok(false);
6757 }
6758 self.advance(); // INITIALLY
6759 match self.advance() {
6760 Token::Ident(s)
6761 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6762 {
6763 Ok(s.eq_ignore_ascii_case("deferred"))
6764 }
6765 other => Err(self.err(alloc::format!(
6766 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6767 ))),
6768 }
6769 }
6770
6771 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6772 /// in its entirety so the parser returns Empty without
6773 /// touching the runtime. The CREATE+PROCEDURE keywords are
6774 /// already consumed; this swallows everything from the
6775 /// procedure name through the matching `END`, including
6776 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6777 /// (DELIMITER `//` makes the script splitter forward the
6778 /// whole block as one statement), `@var` session-variable
6779 /// references, and the trailing terminator.
6780 ///
6781 /// Tracks nesting depth so:
6782 /// BEGIN
6783 /// IF cond THEN
6784 /// BEGIN ... END;
6785 /// END IF;
6786 /// END
6787 /// terminates at the outer END.
6788 fn consume_mysql_routine_body(&mut self) {
6789 // Outer skeleton: name, (...), optional clauses, BEGIN
6790 // <body> END [;]. Scan for the first BEGIN — anything
6791 // before it is signature decoration we don't care about.
6792 // Once inside BEGIN, count up on BEGIN, down on END.
6793 let mut depth: i32 = 0;
6794 let mut started = false;
6795 loop {
6796 match self.peek().clone() {
6797 Token::Begin => {
6798 self.advance();
6799 depth += 1;
6800 started = true;
6801 }
6802 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6803 self.advance();
6804 if started {
6805 depth -= 1;
6806 if depth <= 0 {
6807 // Optional trailing ident (`END IF`,
6808 // `END LOOP`, `END WHILE`, `END CASE`,
6809 // `END label_name`) — eat the next
6810 // ident if present so we don't
6811 // mistake `END IF;` for the outer
6812 // close.
6813 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6814 // If the next token is one of the
6815 // PL/SQL block-closer keywords,
6816 // the END belongs to an inner
6817 // block; bump depth back up.
6818 let is_inner_close = matches!(
6819 self.peek(),
6820 Token::Ident(s) | Token::QuotedIdent(s)
6821 if matches!(
6822 s.to_ascii_lowercase().as_str(),
6823 "if" | "loop" | "while" | "case" | "repeat"
6824 )
6825 );
6826 if is_inner_close {
6827 self.advance();
6828 depth += 1;
6829 continue;
6830 }
6831 }
6832 // Eat optional trailing `;`.
6833 if matches!(self.peek(), Token::Semicolon) {
6834 self.advance();
6835 }
6836 return;
6837 }
6838 }
6839 }
6840 Token::Eof => return,
6841 _ => {
6842 self.advance();
6843 }
6844 }
6845 }
6846 }
6847
6848 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6849 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6850 ///
6851 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6852 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6853 /// ident, or `ident @ ident-or-quoted-string` host form)
6854 /// * `SQL SECURITY {DEFINER|INVOKER}`
6855 ///
6856 /// Each clause may appear at most once but in any order.
6857 /// The hints are pure planner / permission metadata that
6858 /// SPG's view-rewrite engine handles uniformly; we accept
6859 /// and discard. Returns `Ok(())` once a non-clause token is
6860 /// peeked (the caller then checks for the `VIEW` keyword).
6861 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6862 loop {
6863 match self.peek().clone() {
6864 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6865 self.advance(); // ALGORITHM
6866 // Optional `=`. MySQL spec requires it but be
6867 // generous.
6868 if matches!(self.peek(), Token::Eq) {
6869 self.advance();
6870 }
6871 // UNDEFINED / MERGE / TEMPTABLE — accept any
6872 // bare ident; unknown values still parse so
6873 // future MySQL versions don't break.
6874 if matches!(
6875 self.peek(),
6876 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6877 ) {
6878 self.advance();
6879 }
6880 }
6881 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6882 self.advance(); // DEFINER
6883 if matches!(self.peek(), Token::Eq) {
6884 self.advance();
6885 }
6886 // User: quoted string, ident, OR ident @ host
6887 // (host may itself be quoted or bare).
6888 match self.peek().clone() {
6889 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6890 self.advance();
6891 // Optional `@host`.
6892 if matches!(self.peek(), Token::At) {
6893 self.advance();
6894 if matches!(
6895 self.peek(),
6896 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6897 ) {
6898 self.advance();
6899 }
6900 }
6901 }
6902 _ => {}
6903 }
6904 }
6905 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6906 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6907 // when followed by SECURITY — the dispatcher must
6908 // not consume a bare `SQL` token (it's not a
6909 // legal CREATE prefix on its own).
6910 let save = self.pos;
6911 self.advance(); // SQL
6912 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6913 if s2.eq_ignore_ascii_case("security"))
6914 {
6915 self.advance(); // SECURITY
6916 // DEFINER / INVOKER trailing ident.
6917 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6918 self.advance();
6919 }
6920 } else {
6921 // Not a SQL SECURITY clause — roll back and
6922 // bail; the caller will error out cleanly.
6923 self.pos = save;
6924 return Ok(());
6925 }
6926 }
6927 _ => return Ok(()),
6928 }
6929 }
6930 }
6931
6932 fn parse_if_not_exists(&mut self) -> bool {
6933 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6934 {
6935 let save = self.pos;
6936 self.advance();
6937 if matches!(self.peek(), Token::Not) {
6938 self.advance();
6939 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6940 {
6941 self.advance();
6942 return true;
6943 }
6944 }
6945 self.pos = save;
6946 }
6947 false
6948 }
6949
6950 fn parse_if_exists(&mut self) -> bool {
6951 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6952 {
6953 let save = self.pos;
6954 self.advance();
6955 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6956 {
6957 self.advance();
6958 return true;
6959 }
6960 self.pos = save;
6961 }
6962 false
6963 }
6964
6965 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6966 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6967 /// been consumed.
6968 fn parse_create_trigger_after_keyword(
6969 &mut self,
6970 or_replace: bool,
6971 ) -> Result<Statement, ParseError> {
6972 let name = self.expect_ident_like()?;
6973 let timing = {
6974 let ident = self.expect_ident_like()?;
6975 if ident.eq_ignore_ascii_case("before") {
6976 TriggerTiming::Before
6977 } else if ident.eq_ignore_ascii_case("after") {
6978 TriggerTiming::After
6979 } else if ident.eq_ignore_ascii_case("instead") {
6980 let next = self.expect_ident_like()?;
6981 if !next.eq_ignore_ascii_case("of") {
6982 return Err(self.err(alloc::format!(
6983 "expected OF after INSTEAD in trigger timing, got {next:?}"
6984 )));
6985 }
6986 TriggerTiming::InsteadOf
6987 } else {
6988 return Err(self.err(alloc::format!(
6989 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6990 )));
6991 }
6992 };
6993 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6994 // OR is a reserved keyword token (Token::Or), not an Ident.
6995 // v7.13.0 — after an UPDATE event we may optionally see
6996 // `OF col, col, …` (mailrs round-5 G7). Columns are
6997 // captured into `update_columns` once across the whole
6998 // events list; multiple `UPDATE OF` clauses are rejected.
6999 let mut events: Vec<TriggerEvent> = Vec::new();
7000 let mut update_columns: Vec<String> = Vec::new();
7001 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
7002 events.push(first_ev);
7003 if !first_cols.is_empty() {
7004 update_columns = first_cols;
7005 }
7006 while matches!(self.peek(), Token::Or) {
7007 self.advance();
7008 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
7009 events.push(ev);
7010 if !cols.is_empty() {
7011 if !update_columns.is_empty() {
7012 return Err(
7013 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
7014 );
7015 }
7016 update_columns = cols;
7017 }
7018 }
7019 // ON <table>
7020 let tok = self.peek();
7021 let Token::On = tok else {
7022 return Err(self.err(alloc::format!(
7023 "expected ON after trigger events, got {tok:?}"
7024 )));
7025 };
7026 self.advance();
7027 let table = self.expect_ident_like()?;
7028 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
7029 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
7030 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
7031 // the trigger as a plain AFTER trigger (correct for every non-deferred
7032 // use; deferral timing is not yet honoured).
7033 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7034 if s.eq_ignore_ascii_case("from"))
7035 {
7036 self.advance();
7037 let _reftable = self.expect_ident_like()?;
7038 }
7039 self.consume_optional_deferrable_clauses()?;
7040 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
7041 // keyword (Token::For); EACH / ROW / STATEMENT are bare
7042 // idents.
7043 if !matches!(self.peek(), Token::For) {
7044 return Err(self.err(alloc::format!(
7045 "expected FOR EACH ROW / STATEMENT, got {:?}",
7046 self.peek()
7047 )));
7048 }
7049 self.advance();
7050 let for_each = {
7051 let e = self.expect_ident_like()?;
7052 if !e.eq_ignore_ascii_case("each") {
7053 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
7054 }
7055 let unit = self.expect_ident_like()?;
7056 if unit.eq_ignore_ascii_case("row") {
7057 TriggerForEach::Row
7058 } else if unit.eq_ignore_ascii_case("statement") {
7059 TriggerForEach::Statement
7060 } else {
7061 return Err(self.err(alloc::format!(
7062 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
7063 )));
7064 }
7065 };
7066 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
7067 let when_condition = if matches!(self.peek(),
7068 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7069 {
7070 self.advance();
7071 Some(self.parse_paren_expr("WHEN")?)
7072 } else {
7073 None
7074 };
7075 // EXECUTE FUNCTION/PROCEDURE name(...)
7076 let exec = self.expect_ident_like()?;
7077 if !exec.eq_ignore_ascii_case("execute") {
7078 return Err(self.err(alloc::format!(
7079 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7080 )));
7081 }
7082 let fn_or_proc = self.expect_ident_like()?;
7083 if !(fn_or_proc.eq_ignore_ascii_case("function")
7084 || fn_or_proc.eq_ignore_ascii_case("procedure"))
7085 {
7086 return Err(self.err(alloc::format!(
7087 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7088 )));
7089 }
7090 let function = self.expect_ident_like()?;
7091 // Optional empty arg list `()`.
7092 if matches!(self.peek(), Token::LParen) {
7093 self.advance();
7094 if !matches!(self.peek(), Token::RParen) {
7095 return Err(self.err(alloc::format!(
7096 "v7.12.4 trigger function calls take no args; got {:?}",
7097 self.peek()
7098 )));
7099 }
7100 self.advance();
7101 }
7102 Ok(Statement::CreateTrigger(CreateTriggerStatement {
7103 name,
7104 or_replace,
7105 timing,
7106 events,
7107 table,
7108 for_each,
7109 function,
7110 update_columns,
7111 when_condition,
7112 }))
7113 }
7114
7115 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7116 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7117 fn parse_create_rule_after_keyword(
7118 &mut self,
7119 or_replace: bool,
7120 ) -> Result<Statement, ParseError> {
7121 let name = self.expect_ident_like()?;
7122 if !matches!(self.peek(), Token::As) {
7123 return Err(self.err(alloc::format!(
7124 "expected AS in CREATE RULE, got {:?}",
7125 self.peek()
7126 )));
7127 }
7128 self.advance();
7129 if !matches!(self.peek(), Token::On) {
7130 return Err(self.err(alloc::format!(
7131 "expected ON in CREATE RULE, got {:?}",
7132 self.peek()
7133 )));
7134 }
7135 self.advance();
7136 let event = self.parse_rule_event()?;
7137 if !matches!(self.peek(), Token::To)
7138 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7139 {
7140 return Err(self.err(alloc::format!(
7141 "expected TO after rule event, got {:?}",
7142 self.peek()
7143 )));
7144 }
7145 self.advance();
7146 let table = self.expect_ident_like()?;
7147 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7148 let when_condition = if matches!(self.peek(), Token::Where) {
7149 self.advance();
7150 Some(self.parse_expr(0)?)
7151 } else {
7152 None
7153 };
7154 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7155 {
7156 return Err(self.err(alloc::format!(
7157 "expected DO in CREATE RULE, got {:?}",
7158 self.peek()
7159 )));
7160 }
7161 self.advance();
7162 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7163 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7164 {
7165 self.advance();
7166 true
7167 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7168 self.advance();
7169 false
7170 } else {
7171 false
7172 };
7173 // `NOTHING` | `( cmd; … )` | `cmd`.
7174 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7175 {
7176 self.advance();
7177 Vec::new()
7178 } else if matches!(self.peek(), Token::LParen) {
7179 self.advance();
7180 let mut cmds = Vec::new();
7181 loop {
7182 cmds.push(self.parse_one_statement()?);
7183 if matches!(self.peek(), Token::Semicolon) {
7184 self.advance();
7185 if matches!(self.peek(), Token::RParen) {
7186 break;
7187 }
7188 continue;
7189 }
7190 break;
7191 }
7192 if !matches!(self.peek(), Token::RParen) {
7193 return Err(self.err(alloc::format!(
7194 "expected ) closing the CREATE RULE command list, got {:?}",
7195 self.peek()
7196 )));
7197 }
7198 self.advance();
7199 cmds
7200 } else {
7201 alloc::vec![self.parse_one_statement()?]
7202 };
7203 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7204 name,
7205 or_replace,
7206 event,
7207 table,
7208 instead,
7209 when_condition,
7210 commands,
7211 }))
7212 }
7213
7214 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7215 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7216 if matches!(self.peek(), Token::Insert) {
7217 self.advance();
7218 return Ok(alloc::string::String::from("INSERT"));
7219 }
7220 if matches!(self.peek(), Token::Select) {
7221 self.advance();
7222 return Ok(alloc::string::String::from("SELECT"));
7223 }
7224 match self.peek() {
7225 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7226 self.advance();
7227 Ok(alloc::string::String::from("UPDATE"))
7228 }
7229 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7230 self.advance();
7231 Ok(alloc::string::String::from("DELETE"))
7232 }
7233 other => Err(self.err(alloc::format!(
7234 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7235 ))),
7236 }
7237 }
7238
7239 /// v7.13.0 — parse one trigger event, then optionally consume
7240 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7241 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7242 fn parse_trigger_event_with_optional_of(
7243 &mut self,
7244 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7245 let ev = self.parse_trigger_event()?;
7246 if !matches!(ev, TriggerEvent::Update) {
7247 return Ok((ev, Vec::new()));
7248 }
7249 // `OF` is a bare ident.
7250 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7251 return Ok((ev, Vec::new()));
7252 }
7253 self.advance(); // OF
7254 let mut cols: Vec<String> = Vec::new();
7255 loop {
7256 cols.push(self.expect_ident_like()?);
7257 if matches!(self.peek(), Token::Comma) {
7258 self.advance();
7259 continue;
7260 }
7261 break;
7262 }
7263 if cols.is_empty() {
7264 return Err(
7265 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7266 );
7267 }
7268 Ok((ev, cols))
7269 }
7270
7271 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7272 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7273 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7274 /// inside the body.
7275 /// Called by [`parse_plpgsql_body`] after the body's tokens
7276 /// have been lexed into this temporary parser.
7277 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7278 // v7.12.6 — optional DECLARE prelude.
7279 let declarations = if matches!(
7280 self.peek(),
7281 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7282 ) {
7283 self.advance();
7284 self.parse_plpgsql_declare_block()?
7285 } else {
7286 Vec::new()
7287 };
7288 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7289 // `BEGIN` transaction-start, but we can reuse the
7290 // reserved Token::Begin since the body is a separate
7291 // lex/parse context).
7292 if !matches!(self.peek(), Token::Begin) {
7293 return Err(self.err(alloc::format!(
7294 "expected BEGIN at start of plpgsql block, got {:?}",
7295 self.peek()
7296 )));
7297 }
7298 self.advance();
7299 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7300 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7301 // body's last statement and the trailing END. When present
7302 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7303 // arms terminated by END.
7304 let exception_handlers = if matches!(
7305 self.peek(),
7306 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7307 ) {
7308 self.advance();
7309 self.parse_plpgsql_exception_handlers()?
7310 } else {
7311 Vec::new()
7312 };
7313 Ok(PlPgSqlBlock {
7314 declarations,
7315 statements,
7316 exception_handlers,
7317 })
7318 }
7319
7320 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7321 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7322 fn parse_plpgsql_exception_handlers(
7323 &mut self,
7324 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7325 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7326 loop {
7327 // Stop at END — the block-level trailing END LOOP / END;
7328 // is handled by the caller.
7329 if matches!(
7330 self.peek(),
7331 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7332 ) {
7333 return Ok(out);
7334 }
7335 // WHEN <cond> [OR <cond>]* THEN <body>
7336 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7337 {
7338 return Err(self.err(alloc::format!(
7339 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7340 self.peek()
7341 )));
7342 }
7343 self.advance();
7344 let mut conditions: Vec<String> = Vec::new();
7345 conditions.push(self.expect_ident_like()?);
7346 while matches!(self.peek(), Token::Or) {
7347 self.advance();
7348 conditions.push(self.expect_ident_like()?);
7349 }
7350 let then_kw = self.expect_ident_like()?;
7351 if !then_kw.eq_ignore_ascii_case("then") {
7352 return Err(self.err(alloc::format!(
7353 "expected THEN after WHEN condition list, got {then_kw:?}"
7354 )));
7355 }
7356 let body = self.parse_plpgsql_stmt_list_until_end()?;
7357 out.push(crate::ast::ExceptionHandler { conditions, body });
7358 }
7359 }
7360
7361 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7362 /// prelude. Caller has already consumed `DECLARE`. We stop
7363 /// reading entries when we hit `BEGIN`.
7364 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7365 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7366 loop {
7367 if matches!(self.peek(), Token::Begin) {
7368 return Ok(out);
7369 }
7370 let name = self.expect_ident_like()?;
7371 // v7.37.20 (20.7) — type inference: if the next token is
7372 // `:=` or `=` (no explicit type), infer from the default
7373 // expression. Otherwise the ident that follows is the
7374 // declared type.
7375 //
7376 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7377 // (PG-standard). SPG parse-accepts and treats identically
7378 // to inference — the eventual runtime value determines
7379 // the local's type, which is faithful to how SPG handles
7380 // untyped locals today (see 20.7). Full compile-time
7381 // catalog lookup queues with v7.40 PL/pgSQL epic.
7382 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7383 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7384 // downstream declaration walker to type the local by
7385 // the runtime type of the default expression.
7386 FunctionArgType::Raw("_infer_".into())
7387 } else {
7388 let ty_token = self.expect_ident_like()?;
7389 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7390 // consume optional `.<ident>` qualifier + `%<KW>`
7391 // suffix. Both qualifier and suffix map to _infer_.
7392 if matches!(self.peek(), Token::Dot) {
7393 self.advance();
7394 let _ = self.expect_ident_like()?;
7395 }
7396 if matches!(self.peek(), Token::Percent) {
7397 self.advance();
7398 // Consume the trailing TYPE / ROWTYPE ident.
7399 let _ = self.expect_ident_like()?;
7400 FunctionArgType::Raw("_infer_".into())
7401 } else {
7402 match map_type_ident_to_column_type_name(&ty_token) {
7403 Some(t) => FunctionArgType::Typed(t),
7404 None => FunctionArgType::Raw(ty_token),
7405 }
7406 }
7407 };
7408 let default = match self.peek() {
7409 Token::ColonEq => {
7410 self.advance();
7411 Some(self.parse_expr(0)?)
7412 }
7413 Token::Eq => {
7414 // PL/pgSQL also accepts `=` for the
7415 // DECLARE default (PG treats them the same
7416 // in this position).
7417 self.advance();
7418 Some(self.parse_expr(0)?)
7419 }
7420 _ => None,
7421 };
7422 // Mandatory `;` between declarations.
7423 if !matches!(self.peek(), Token::Semicolon) {
7424 return Err(self.err(alloc::format!(
7425 "expected ; after DECLARE entry for {name:?}, got {:?}",
7426 self.peek()
7427 )));
7428 }
7429 self.advance();
7430 out.push(PlPgSqlDeclare { name, ty, default });
7431 }
7432 }
7433
7434 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7435 /// the terminating `END;` (or `END IF;` etc — handled by the
7436 /// per-construct sub-parsers). Used by both the outer block
7437 /// and the IF/ELSE branch bodies.
7438 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7439 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7440 loop {
7441 // Allow trailing semicolons + END.
7442 while matches!(self.peek(), Token::Semicolon) {
7443 self.advance();
7444 }
7445 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7446 if matches!(
7447 self.peek(),
7448 Token::Ident(s) | Token::QuotedIdent(s)
7449 if s.eq_ignore_ascii_case("end")
7450 || s.eq_ignore_ascii_case("else")
7451 || s.eq_ignore_ascii_case("elsif")
7452 || s.eq_ignore_ascii_case("elseif")
7453 || s.eq_ignore_ascii_case("exception")
7454 || s.eq_ignore_ascii_case("when")
7455 ) {
7456 return Ok(statements);
7457 }
7458 // Otherwise: one statement, then expect `;` or
7459 // a block-terminator keyword.
7460 let stmt = self.parse_plpgsql_stmt()?;
7461 statements.push(stmt);
7462 match self.peek() {
7463 Token::Semicolon => {
7464 self.advance();
7465 }
7466 Token::Ident(s) | Token::QuotedIdent(s)
7467 if s.eq_ignore_ascii_case("end")
7468 || s.eq_ignore_ascii_case("else")
7469 || s.eq_ignore_ascii_case("elsif")
7470 || s.eq_ignore_ascii_case("elseif")
7471 || s.eq_ignore_ascii_case("exception")
7472 || s.eq_ignore_ascii_case("when") =>
7473 {
7474 // Final statement of the block without `;`.
7475 }
7476 other => {
7477 return Err(self.err(alloc::format!(
7478 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7479 )));
7480 }
7481 }
7482 }
7483 }
7484
7485 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7486 // RETURN keyword?
7487 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7488 {
7489 self.advance();
7490 return self.parse_plpgsql_return();
7491 }
7492 // v7.12.6 — IF block.
7493 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7494 {
7495 self.advance();
7496 return self.parse_plpgsql_if();
7497 }
7498 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7499 // Detected by peeking that token pos+3 is Ident("execute").
7500 if matches!(self.peek(), Token::For)
7501 && matches!(
7502 self.tokens.get(self.pos + 1),
7503 Some(Token::Ident(_) | Token::QuotedIdent(_))
7504 )
7505 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7506 && matches!(
7507 self.tokens.get(self.pos + 3),
7508 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7509 )
7510 {
7511 self.advance(); // FOR
7512 let var = self.expect_ident_like()?;
7513 self.advance(); // IN
7514 self.advance(); // EXECUTE
7515 // Prescan for LOOP at paren depth 0 so parse_expr stops
7516 // before the LOOP keyword (same trick as the bare-SELECT
7517 // ForQuery arm).
7518 let mut depth: i32 = 0;
7519 let mut loop_pos: Option<usize> = None;
7520 let mut scan = self.pos;
7521 while scan < self.tokens.len() {
7522 match self.tokens.get(scan) {
7523 Some(Token::LParen) => depth += 1,
7524 Some(Token::RParen) => depth -= 1,
7525 Some(Token::Ident(s) | Token::QuotedIdent(s))
7526 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7527 {
7528 loop_pos = Some(scan);
7529 break;
7530 }
7531 _ => {}
7532 }
7533 scan += 1;
7534 }
7535 let loop_pos = loop_pos.ok_or_else(|| {
7536 self.err(alloc::format!(
7537 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7538 ))
7539 })?;
7540 let saved_loop = self.tokens[loop_pos].clone();
7541 self.tokens[loop_pos] = Token::Semicolon;
7542 let expr_result = self.parse_expr(0);
7543 self.tokens[loop_pos] = saved_loop;
7544 let sql_expr = expr_result?;
7545 let loop_kw = self.expect_ident_like()?;
7546 if !loop_kw.eq_ignore_ascii_case("loop") {
7547 return Err(self.err(alloc::format!(
7548 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7549 )));
7550 }
7551 let body = self.parse_plpgsql_stmt_list_until_end()?;
7552 let end_kw = self.expect_ident_like()?;
7553 if !end_kw.eq_ignore_ascii_case("end") {
7554 return Err(self.err(alloc::format!(
7555 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7556 )));
7557 }
7558 let loop_kw2 = self.expect_ident_like()?;
7559 if !loop_kw2.eq_ignore_ascii_case("loop") {
7560 return Err(self.err(alloc::format!(
7561 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7562 )));
7563 }
7564 return Ok(PlPgSqlStmt::ForExecute {
7565 var,
7566 sql_expr,
7567 body,
7568 });
7569 }
7570 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7571 //
7572 // Two syntactic forms:
7573 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7574 // FOR var IN (SELECT ...) LOOP ...
7575 //
7576 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7577 // the trailing `LOOP` keyword as a table alias, we prescan
7578 // forward to find LOOP at paren depth 0, splice a fake
7579 // Semicolon at that position (so SELECT parses cleanly),
7580 // then re-splice LOOP back in.
7581 //
7582 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7583 // LOOP directly — no scan required.
7584 if matches!(self.peek(), Token::For)
7585 && matches!(
7586 self.tokens.get(self.pos + 1),
7587 Some(Token::Ident(_) | Token::QuotedIdent(_))
7588 )
7589 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7590 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7591 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7592 {
7593 self.advance(); // FOR
7594 let var = self.expect_ident_like()?;
7595 // IN
7596 self.advance();
7597 let query = if matches!(self.peek(), Token::LParen) {
7598 // Paren-wrapped SELECT.
7599 self.advance();
7600 let inner = self.parse_select_stmt()?;
7601 let Statement::Select(q) = inner else {
7602 return Err(self.err(alloc::format!(
7603 "expected SELECT inside (…), got {:?}",
7604 self.peek()
7605 )));
7606 };
7607 if !matches!(self.peek(), Token::RParen) {
7608 return Err(self.err(alloc::format!(
7609 "expected ')' after FOR-IN-SELECT body, got {:?}",
7610 self.peek()
7611 )));
7612 }
7613 self.advance();
7614 q
7615 } else {
7616 // Bare SELECT: prescan to find the LOOP boundary.
7617 let mut depth: i32 = 0;
7618 let mut loop_pos: Option<usize> = None;
7619 let mut scan = self.pos;
7620 while scan < self.tokens.len() {
7621 match self.tokens.get(scan) {
7622 Some(Token::LParen) => depth += 1,
7623 Some(Token::RParen) => depth -= 1,
7624 Some(Token::Ident(s) | Token::QuotedIdent(s))
7625 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7626 {
7627 loop_pos = Some(scan);
7628 break;
7629 }
7630 _ => {}
7631 }
7632 scan += 1;
7633 }
7634 let loop_pos = loop_pos.ok_or_else(|| {
7635 self.err(alloc::format!(
7636 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7637 ))
7638 })?;
7639 // Swap the LOOP token with a synthetic Semicolon so
7640 // parse_select_stmt stops there, then restore afterward.
7641 let saved_loop = self.tokens[loop_pos].clone();
7642 self.tokens[loop_pos] = Token::Semicolon;
7643 let parse_result = self.parse_select_stmt();
7644 self.tokens[loop_pos] = saved_loop;
7645 let inner = parse_result?;
7646 let Statement::Select(q) = inner else {
7647 return Err(self.err(alloc::format!(
7648 "expected SELECT after FOR <var> IN, got {:?}",
7649 self.peek()
7650 )));
7651 };
7652 q
7653 };
7654 let loop_kw = self.expect_ident_like()?;
7655 if !loop_kw.eq_ignore_ascii_case("loop") {
7656 return Err(self.err(alloc::format!(
7657 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7658 )));
7659 }
7660 let body = self.parse_plpgsql_stmt_list_until_end()?;
7661 let end_kw = self.expect_ident_like()?;
7662 if !end_kw.eq_ignore_ascii_case("end") {
7663 return Err(self.err(alloc::format!(
7664 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7665 )));
7666 }
7667 let loop_kw2 = self.expect_ident_like()?;
7668 if !loop_kw2.eq_ignore_ascii_case("loop") {
7669 return Err(self.err(alloc::format!(
7670 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7671 )));
7672 }
7673 return Ok(PlPgSqlStmt::ForQuery {
7674 var,
7675 query: Box::new(query),
7676 body,
7677 });
7678 }
7679 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7680 // FOR is a reserved keyword token (Token::For).
7681 if matches!(self.peek(), Token::For)
7682 && matches!(
7683 self.tokens.get(self.pos + 1),
7684 Some(Token::Ident(_) | Token::QuotedIdent(_))
7685 )
7686 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7687 {
7688 self.advance(); // FOR
7689 let var = self.expect_ident_like()?;
7690 if !matches!(self.peek(), Token::In) {
7691 return Err(self.err(alloc::format!(
7692 "expected IN after FOR <var>, got {:?}",
7693 self.peek()
7694 )));
7695 }
7696 self.advance();
7697 let reverse = matches!(
7698 self.peek(),
7699 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7700 );
7701 if reverse {
7702 self.advance();
7703 }
7704 let start = self.parse_expr(0)?;
7705 if !matches!(self.peek(), Token::DotDot) {
7706 return Err(self.err(alloc::format!(
7707 "expected '..' between FOR loop bounds, got {:?}",
7708 self.peek()
7709 )));
7710 }
7711 self.advance();
7712 let end = self.parse_expr(0)?;
7713 let loop_kw = self.expect_ident_like()?;
7714 if !loop_kw.eq_ignore_ascii_case("loop") {
7715 return Err(self.err(alloc::format!(
7716 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7717 )));
7718 }
7719 let body = self.parse_plpgsql_stmt_list_until_end()?;
7720 let end_kw = self.expect_ident_like()?;
7721 if !end_kw.eq_ignore_ascii_case("end") {
7722 return Err(self.err(alloc::format!(
7723 "expected END LOOP after FOR body, got {end_kw:?}"
7724 )));
7725 }
7726 let loop_kw2 = self.expect_ident_like()?;
7727 if !loop_kw2.eq_ignore_ascii_case("loop") {
7728 return Err(self.err(alloc::format!(
7729 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7730 )));
7731 }
7732 return Ok(PlPgSqlStmt::ForRange {
7733 var,
7734 start,
7735 end,
7736 reverse,
7737 body,
7738 });
7739 }
7740 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7741 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7742 {
7743 self.advance();
7744 let body = self.parse_plpgsql_stmt_list_until_end()?;
7745 let end_kw = self.expect_ident_like()?;
7746 if !end_kw.eq_ignore_ascii_case("end") {
7747 return Err(self.err(alloc::format!(
7748 "expected END LOOP after LOOP body, got {end_kw:?}"
7749 )));
7750 }
7751 let loop_kw = self.expect_ident_like()?;
7752 if !loop_kw.eq_ignore_ascii_case("loop") {
7753 return Err(self.err(alloc::format!(
7754 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7755 )));
7756 }
7757 return Ok(PlPgSqlStmt::Loop { body });
7758 }
7759 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7760 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7761 {
7762 self.advance();
7763 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7764 {
7765 self.advance();
7766 Some(self.parse_expr(0)?)
7767 } else {
7768 None
7769 };
7770 return Ok(PlPgSqlStmt::Exit { when });
7771 }
7772 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7773 // already-parsed Statement or a runtime-computed SQL string.
7774 // The disambiguator vs the extended-query-protocol `EXECUTE
7775 // <stmt_name>` (which is a top-level Statement, not a
7776 // plpgsql line) is that inside a DO block / trigger body the
7777 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7778 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7779 {
7780 self.advance();
7781 let sql = self.parse_expr(0)?;
7782 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7783 }
7784 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7785 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7786 {
7787 self.advance();
7788 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7789 {
7790 self.advance();
7791 Some(self.parse_expr(0)?)
7792 } else {
7793 None
7794 };
7795 return Ok(PlPgSqlStmt::Continue { when });
7796 }
7797 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7798 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7799 {
7800 self.advance();
7801 let condition = self.parse_expr(0)?;
7802 let loop_kw = self.expect_ident_like()?;
7803 if !loop_kw.eq_ignore_ascii_case("loop") {
7804 return Err(self.err(alloc::format!(
7805 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7806 )));
7807 }
7808 let body = self.parse_plpgsql_stmt_list_until_end()?;
7809 // Expect END LOOP.
7810 let end_kw = self.expect_ident_like()?;
7811 if !end_kw.eq_ignore_ascii_case("end") {
7812 return Err(self.err(alloc::format!(
7813 "expected END LOOP after WHILE body, got {end_kw:?}"
7814 )));
7815 }
7816 let loop_kw2 = self.expect_ident_like()?;
7817 if !loop_kw2.eq_ignore_ascii_case("loop") {
7818 return Err(self.err(alloc::format!(
7819 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7820 )));
7821 }
7822 return Ok(PlPgSqlStmt::While { condition, body });
7823 }
7824 // v7.12.6 — RAISE.
7825 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7826 {
7827 self.advance();
7828 return self.parse_plpgsql_raise();
7829 }
7830 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7831 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7832 {
7833 self.advance();
7834 let condition = self.parse_expr(0)?;
7835 let message = if matches!(self.peek(), Token::Comma) {
7836 self.advance();
7837 Some(self.parse_expr(0)?)
7838 } else {
7839 None
7840 };
7841 return Ok(PlPgSqlStmt::Assert { condition, message });
7842 }
7843 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7844 // "PERFORM is equivalent to SELECT but discards the
7845 // result." Side effects (function calls, RAISE inside
7846 // SQL functions, etc.) still execute. We desugar to
7847 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7848 // existing embedded-statement path handles execution +
7849 // result-discard cleanly. The result is naturally
7850 // discarded because EmbeddedSql doesn't propagate row
7851 // sets back to the plpgsql interpreter.
7852 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7853 {
7854 self.advance();
7855 // Splice a synthetic Token::Select into the stream at
7856 // the current position so parse_select_stmt parses the
7857 // remainder as a normal SELECT body. Token-stream
7858 // surgery mirrors the try_parse_plpgsql_select_into
7859 // pattern used for SELECT … INTO desugaring.
7860 self.tokens.insert(self.pos, Token::Select);
7861 let select = self.parse_select_stmt()?;
7862 let Statement::Select(s) = select else {
7863 return Err(self.err(alloc::format!(
7864 "expected SELECT body after PERFORM, got {:?}",
7865 self.peek()
7866 )));
7867 };
7868 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7869 }
7870 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7871 // plpgsql-specific shape (mailrs round-10 migrate-042).
7872 // PG's SELECT INTO at top-level SQL would CREATE a new
7873 // table; inside plpgsql it ASSIGNS the query result to
7874 // a local variable. We detect the INTO at paren-depth
7875 // 0 between SELECT and the statement boundary; if
7876 // found, split the token stream into "pre-INTO
7877 // projection" + "var" + "post-INTO FROM/WHERE…" and
7878 // rebuild as a SelectInto with a regular SELECT body
7879 // (no INTO clause).
7880 if matches!(self.peek(), Token::Select)
7881 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7882 {
7883 return Ok(PlPgSqlStmt::SelectInto {
7884 var: var_name,
7885 body: Box::new(select_body),
7886 });
7887 }
7888 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7889 // SELECT can appear directly inside a trigger body; we
7890 // recurse into the regular Statement parser, which will
7891 // stop at the trailing `;` (which our caller then
7892 // consumes).
7893 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7894 // also embed ALTER / CREATE / DROP statements; route
7895 // those through the same parser so the DO body parses
7896 // cleanly.
7897 if matches!(self.peek(), Token::Insert)
7898 || matches!(self.peek(), Token::Select)
7899 || matches!(self.peek(), Token::Create)
7900 || matches!(self.peek(), Token::Drop)
7901 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7902 if s.eq_ignore_ascii_case("update")
7903 || s.eq_ignore_ascii_case("delete")
7904 || s.eq_ignore_ascii_case("alter"))
7905 {
7906 let stmt = self.parse_one_statement()?;
7907 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7908 }
7909 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7910 // followed by `:=` and an expression.
7911 let target = self.parse_plpgsql_assign_target()?;
7912 // PL/pgSQL assignment uses `:=`. The lexer represents
7913 // this as a colon followed by `=`; check both shapes.
7914 match self.peek() {
7915 Token::ColonEq => {
7916 self.advance();
7917 }
7918 Token::Colon => {
7919 self.advance();
7920 if !matches!(self.peek(), Token::Eq) {
7921 return Err(self.err(alloc::format!(
7922 "expected := after plpgsql assign target, got `:` then {:?}",
7923 self.peek()
7924 )));
7925 }
7926 self.advance();
7927 }
7928 other => {
7929 return Err(self.err(alloc::format!(
7930 "expected := after plpgsql assign target, got {other:?}"
7931 )));
7932 }
7933 }
7934 let value = self.parse_expr(0)?;
7935 Ok(PlPgSqlStmt::Assign { target, value })
7936 }
7937
7938 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7939 /// [ELSE body] END IF`. `IF` keyword already consumed.
7940 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7941 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7942 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7943 loop {
7944 // <expr> THEN
7945 let cond = self.parse_expr(0)?;
7946 let then_kw = self.expect_ident_like()?;
7947 if !then_kw.eq_ignore_ascii_case("then") {
7948 return Err(self.err(alloc::format!(
7949 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7950 )));
7951 }
7952 let body = self.parse_plpgsql_stmt_list_until_end()?;
7953 branches.push((cond, body));
7954 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7955 match self.peek() {
7956 Token::Ident(s) | Token::QuotedIdent(s)
7957 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7958 {
7959 self.advance();
7960 continue;
7961 }
7962 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7963 self.advance();
7964 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7965 break;
7966 }
7967 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7968 break;
7969 }
7970 other => {
7971 return Err(self.err(alloc::format!(
7972 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7973 )));
7974 }
7975 }
7976 }
7977 // Expect `END IF` (the END keyword is the one we're
7978 // looking at right now).
7979 let end_kw = self.expect_ident_like()?;
7980 if !end_kw.eq_ignore_ascii_case("end") {
7981 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7982 }
7983 let if_kw = self.expect_ident_like()?;
7984 if !if_kw.eq_ignore_ascii_case("if") {
7985 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7986 }
7987 Ok(PlPgSqlStmt::If {
7988 branches,
7989 else_branch,
7990 })
7991 }
7992
7993 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7994 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7995 /// is already consumed.
7996 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7997 let lvl_ident = self.expect_ident_like()?;
7998 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7999 "notice" => RaiseLevel::Notice,
8000 "warning" => RaiseLevel::Warning,
8001 "info" => RaiseLevel::Info,
8002 "log" => RaiseLevel::Log,
8003 "debug" => RaiseLevel::Debug,
8004 "exception" => RaiseLevel::Exception,
8005 other => {
8006 return Err(self.err(alloc::format!(
8007 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
8008 )));
8009 }
8010 };
8011 // Message: required for v7.12.6. PG accepts a bare
8012 // RAISE-rethrow form (no message), reserved for future
8013 // RAISE-no-args support.
8014 let Token::String(msg) = self.peek() else {
8015 return Err(self.err(alloc::format!(
8016 "expected RAISE message string, got {:?}",
8017 self.peek()
8018 )));
8019 };
8020 let message = msg.clone();
8021 self.advance();
8022 // Optional comma-separated args (PG `%` format substitution).
8023 let mut args: Vec<Expr> = Vec::new();
8024 while matches!(self.peek(), Token::Comma) {
8025 self.advance();
8026 args.push(self.parse_expr(0)?);
8027 }
8028 Ok(PlPgSqlStmt::Raise {
8029 level,
8030 message,
8031 args,
8032 })
8033 }
8034
8035 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
8036 /// <projection> INTO <var> [FROM …]` (mailrs round-10
8037 /// migrate-042). Returns `(rebuilt_select_without_into,
8038 /// var_name)` when the pattern matches; `None` for
8039 /// regular SELECTs (those go through the embedded-SQL
8040 /// path). Token-stream surgery so the rebuilt SELECT
8041 /// parses through the regular `parse_select_stmt`.
8042 #[allow(clippy::too_many_lines)]
8043 fn try_parse_plpgsql_select_into(
8044 &mut self,
8045 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
8046 // Scan forward from `self.pos + 1` (past Token::Select)
8047 // for Token::Into at paren-depth 0, stopping at the
8048 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
8049 // end the plpgsql statement.
8050 let start = self.pos;
8051 let mut into_pos: Option<usize> = None;
8052 let mut depth: i32 = 0;
8053 let mut i = start + 1;
8054 while i < self.tokens.len() {
8055 match &self.tokens[i] {
8056 Token::LParen => depth += 1,
8057 Token::RParen => depth -= 1,
8058 Token::Semicolon if depth == 0 => break,
8059 Token::Ident(s)
8060 if depth == 0
8061 && (s.eq_ignore_ascii_case("end")
8062 || s.eq_ignore_ascii_case("else")
8063 || s.eq_ignore_ascii_case("elsif")) =>
8064 {
8065 break;
8066 }
8067 Token::Into if depth == 0 => {
8068 into_pos = Some(i);
8069 break;
8070 }
8071 _ => {}
8072 }
8073 i += 1;
8074 }
8075 let Some(into_at) = into_pos else {
8076 return Ok(None);
8077 };
8078 // The token immediately after INTO must be the target
8079 // var ident; anything else (e.g. INSERT INTO table)
8080 // ruled out by the depth-0 check above. Capture it.
8081 let var = match self.tokens.get(into_at + 1) {
8082 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8083 other => {
8084 return Err(self.err(alloc::format!(
8085 "expected variable name after SELECT … INTO, got {other:?}"
8086 )));
8087 }
8088 };
8089 // Find the end of the plpgsql SELECT INTO statement —
8090 // same boundary rules as the depth-0 scan above.
8091 let mut end = into_at + 2;
8092 let mut depth2: i32 = 0;
8093 while end < self.tokens.len() {
8094 match &self.tokens[end] {
8095 Token::LParen => depth2 += 1,
8096 Token::RParen => depth2 -= 1,
8097 Token::Semicolon if depth2 == 0 => break,
8098 Token::Ident(s)
8099 if depth2 == 0
8100 && (s.eq_ignore_ascii_case("end")
8101 || s.eq_ignore_ascii_case("else")
8102 || s.eq_ignore_ascii_case("elsif")) =>
8103 {
8104 break;
8105 }
8106 _ => {}
8107 }
8108 end += 1;
8109 }
8110 // Rebuild a token stream that represents the SELECT
8111 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8112 // post-var tokens up to statement end]. Run the
8113 // regular `parse_select_stmt` against it.
8114 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8115 for j in start..into_at {
8116 rebuilt.push(self.tokens[j].clone());
8117 }
8118 for j in (into_at + 2)..end {
8119 rebuilt.push(self.tokens[j].clone());
8120 }
8121 rebuilt.push(Token::Eof);
8122 let saved_pos = self.pos;
8123 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8124 self.pos = 0;
8125 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8126 if !matches!(self.peek(), Token::Select) {
8127 self.tokens = saved_tokens;
8128 self.pos = saved_pos;
8129 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8130 }
8131 let sel = self.parse_select_stmt();
8132 self.tokens = saved_tokens;
8133 self.pos = end;
8134 let sel = sel?;
8135 let Statement::Select(body) = sel else {
8136 return Err(self.err(alloc::format!(
8137 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8138 )));
8139 };
8140 Ok(Some((body, var)))
8141 }
8142
8143 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8144 // v7.16.1 — read the head token DIRECTLY rather than
8145 // via `expect_ident_like`. The v7.14.0 schema-qualifier
8146 // strip (`public.t` → `t`) inside `expect_ident_like`
8147 // greedily consumes any `ident . ident` pair, which
8148 // silently turned every `NEW.col := …` /
8149 // `OLD.col := …` plpgsql assignment into a Local("col")
8150 // assignment — the head "new"/"old" was eaten as if it
8151 // were a schema name and the Dot was consumed too, so
8152 // this function's own `peek() == Token::Dot` check
8153 // below never fired. Every BEFORE trigger that rewrote
8154 // a NEW cell was a silent no-op for two major releases
8155 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8156 // gate failures were investigated as v7.16.1 backlog.
8157 let head = match self.advance() {
8158 Token::Ident(s) | Token::QuotedIdent(s) => s,
8159 other => {
8160 return Err(self.err(alloc::format!(
8161 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8162 )));
8163 }
8164 };
8165 if matches!(self.peek(), Token::Dot) {
8166 self.advance();
8167 let col = self.expect_ident_like()?;
8168 if head.eq_ignore_ascii_case("new") {
8169 return Ok(AssignTarget::NewColumn(col));
8170 }
8171 if head.eq_ignore_ascii_case("old") {
8172 return Ok(AssignTarget::OldColumn(col));
8173 }
8174 return Err(self.err(alloc::format!(
8175 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8176 got {head:?}.<col>"
8177 )));
8178 }
8179 Ok(AssignTarget::Local(head))
8180 }
8181
8182 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8183 // RETURN NEW / OLD / NULL — bare-ident forms.
8184 match self.peek() {
8185 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8186 self.advance();
8187 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8188 }
8189 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8190 self.advance();
8191 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8192 }
8193 Token::Null => {
8194 self.advance();
8195 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8196 }
8197 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8198 // per PL/pgSQL convention.
8199 Token::Semicolon => {
8200 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8201 }
8202 _ => {}
8203 }
8204 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8205 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8206 // caller-visible effect (blocks don't return sets), so we
8207 // desugar it identically to PERFORM: parse the SELECT (or
8208 // EXECUTE dynamic) as embedded SQL that runs for side
8209 // effects and discards the result. RETURN NEXT <expr>
8210 // (single-row accumulator) queues with v7.40 SETOF function
8211 // infrastructure.
8212 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8213 // and keep going.
8214 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8215 {
8216 self.advance();
8217 let e = self.parse_expr(0)?;
8218 return Ok(PlPgSqlStmt::ReturnNext(e));
8219 }
8220 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8221 {
8222 self.advance();
8223 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8224 // rows go to the set, like the static form. It used to desugar to a
8225 // bare ExecuteDynamic, whose result was DISCARDED.
8226 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8227 {
8228 self.advance();
8229 let sql = self.parse_expr(0)?;
8230 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8231 }
8232 // Bare RETURN QUERY <select>. If the current token is
8233 // not already SELECT (e.g., the user wrote `RETURN QUERY
8234 // <projection> FROM ...` in a shorthand — rare but PG
8235 // accepts a bare projection here), splice one in. Same
8236 // trick as PERFORM.
8237 if !matches!(self.peek(), Token::Select) {
8238 self.tokens.insert(self.pos, Token::Select);
8239 }
8240 let select = self.parse_select_stmt()?;
8241 let Statement::Select(s) = select else {
8242 return Err(self.err(alloc::format!(
8243 "expected SELECT body after RETURN QUERY, got {:?}",
8244 self.peek()
8245 )));
8246 };
8247 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8248 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8249 // in a SETOF function is the entire answer thrown away.
8250 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8251 }
8252 // Fall through: parse a full expression.
8253 let e = self.parse_expr(0)?;
8254 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8255 }
8256
8257 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8258 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8259 // are ident-shaped (the parser keys off case-insensitive
8260 // match — same shape used by the top-level Update / Delete
8261 // dispatchers at parse_one_statement).
8262 if matches!(self.peek(), Token::Insert) {
8263 self.advance();
8264 return Ok(TriggerEvent::Insert);
8265 }
8266 match self.peek() {
8267 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8268 self.advance();
8269 Ok(TriggerEvent::Update)
8270 }
8271 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8272 self.advance();
8273 Ok(TriggerEvent::Delete)
8274 }
8275 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8276 self.advance();
8277 Ok(TriggerEvent::Truncate)
8278 }
8279 other => Err(self.err(alloc::format!(
8280 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8281 ))),
8282 }
8283 }
8284
8285 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8286 /// - (no clause) → implicit `FOR ALL TABLES`
8287 /// - `FOR ALL TABLES`
8288 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8289 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8290 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8291 /// REJECTS the bare plural (`invalid publication object list`,
8292 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8293 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8294 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8295 let name = self.expect_ident_or_string()?;
8296 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8297 // shape so existing publications keep parsing identically.
8298 let scope = if matches!(self.peek(), Token::For) {
8299 self.advance();
8300 if matches!(self.peek(), Token::All) {
8301 self.advance();
8302 if !matches!(self.peek(), Token::Tables) {
8303 return Err(self.err(format!(
8304 "expected TABLES after FOR ALL, got {:?}",
8305 self.peek()
8306 )));
8307 }
8308 self.advance();
8309 if matches!(self.peek(), Token::Except) {
8310 self.advance();
8311 let tables = self.parse_publication_table_list()?;
8312 PublicationScope::AllTablesExcept(tables)
8313 } else {
8314 PublicationScope::AllTables
8315 }
8316 } else if matches!(self.peek(), Token::Table) {
8317 self.advance();
8318 let tables = self.parse_publication_table_list()?;
8319 PublicationScope::ForTables(tables)
8320 } else if matches!(self.peek(), Token::Tables) {
8321 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8322 // plural (`FOR TABLES t`) is REJECTED (`invalid
8323 // publication object list`); TABLES only pairs with
8324 // `IN SCHEMA`. The old arm accepted it on an
8325 // unverifiable "PG 19 accepts both" claim.
8326 self.advance();
8327 if !matches!(self.peek(), Token::In) {
8328 return Err(self.err(alloc::string::String::from(
8329 "invalid publication object list",
8330 )));
8331 }
8332 self.advance();
8333 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8334 return Err(self.err(format!(
8335 "expected SCHEMA after FOR TABLES IN, got {:?}",
8336 self.peek()
8337 )));
8338 }
8339 self.advance();
8340 let schema = self.expect_ident_or_string()?;
8341 PublicationScope::TablesInSchema(schema)
8342 } else {
8343 return Err(self.err(format!(
8344 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8345 self.peek()
8346 )));
8347 }
8348 } else {
8349 PublicationScope::AllTables
8350 };
8351 Ok(Statement::CreatePublication(CreatePublicationStatement {
8352 name,
8353 scope,
8354 }))
8355 }
8356
8357 /// v6.1.3 — Comma-separated identifier list for the publication
8358 /// FOR-clause. Requires at least one entry; empty list is a
8359 /// parse error (PG behaviour). Quoted idents are accepted; the
8360 /// names round-trip through `Display` as `quote_ident(name)`.
8361 ///
8362 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8363 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8364 /// pg_dump output. SPG's publication state today is per-table
8365 /// only (matching the pre-PG-15 surface); the col list + WHERE
8366 /// are parsed so dumps load through and the table name reaches
8367 /// `PublicationScope::ForTables`, but the filter is not enforced
8368 /// at publish time. Re-open when a customer dogfood gate
8369 /// requires per-row-filter or column-subset publish semantics
8370 /// (which gates on persistent slot state landing first, 21.12).
8371 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8372 let first = self.parse_publication_table_entry()?;
8373 let mut out = alloc::vec![first];
8374 while matches!(self.peek(), Token::Comma) {
8375 self.advance();
8376 out.push(self.parse_publication_table_entry()?);
8377 }
8378 Ok(out)
8379 }
8380
8381 /// One table entry inside a FOR TABLE clause:
8382 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8383 /// Returns just the table name; the column list + WHERE predicate
8384 /// are consumed and discarded per the parse-accept-discard
8385 /// commitment above.
8386 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8387 let name = self.expect_ident_like()?;
8388 // Optional column list — `(col, col, …)`.
8389 if matches!(self.peek(), Token::LParen) {
8390 self.advance();
8391 // Empty parens are a PG error too; require ≥ 1 column.
8392 let _ = self.expect_ident_like()?;
8393 while matches!(self.peek(), Token::Comma) {
8394 self.advance();
8395 let _ = self.expect_ident_like()?;
8396 }
8397 if !matches!(self.peek(), Token::RParen) {
8398 return Err(self.err(alloc::format!(
8399 "expected ')' to close publication column list, got {:?}",
8400 self.peek()
8401 )));
8402 }
8403 self.advance();
8404 }
8405 // Optional row filter — `WHERE (predicate)`.
8406 if matches!(self.peek(), Token::Where) {
8407 self.advance();
8408 if !matches!(self.peek(), Token::LParen) {
8409 return Err(self.err(alloc::format!(
8410 "expected '(' after WHERE in publication row filter, got {:?}",
8411 self.peek()
8412 )));
8413 }
8414 self.advance();
8415 let _ = self.parse_expr(0)?;
8416 if !matches!(self.peek(), Token::RParen) {
8417 return Err(self.err(alloc::format!(
8418 "expected ')' to close publication WHERE filter, got {:?}",
8419 self.peek()
8420 )));
8421 }
8422 self.advance();
8423 }
8424 Ok(name)
8425 }
8426
8427 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8428 /// CONNECTION '<conn>'
8429 /// PUBLICATION <pub> [, <pub> ...]`.
8430 ///
8431 /// The clause order is fixed (CONNECTION first, then
8432 /// PUBLICATION) to match PG. No WITH-options accepted in
8433 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8434 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8435 let name = self.expect_ident_or_string()?;
8436 if !matches!(self.peek(), Token::Connection) {
8437 return Err(self.err(format!(
8438 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8439 self.peek()
8440 )));
8441 }
8442 self.advance();
8443 let conn_str = self.expect_string_literal()?;
8444 if !matches!(self.peek(), Token::Publication) {
8445 return Err(self.err(format!(
8446 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8447 self.peek()
8448 )));
8449 }
8450 self.advance();
8451 // Reuse the publication FOR-list parser shape: at least one
8452 // identifier, comma-separated.
8453 let first = self.expect_ident_like()?;
8454 let mut publications = alloc::vec![first];
8455 while matches!(self.peek(), Token::Comma) {
8456 self.advance();
8457 publications.push(self.expect_ident_like()?);
8458 }
8459 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8460 name,
8461 conn_str,
8462 publications,
8463 }))
8464 }
8465
8466 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8467 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8468 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8469 /// that fit `u64`.
8470 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8471 /// qualifier is a *namespace* the app owns (`app.user_id`,
8472 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8473 /// to discard. So parse the raw segments here instead of
8474 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8475 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8476 /// a single segment and round-trip unchanged.
8477 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8478 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8479 loop {
8480 let seg = match self.advance() {
8481 Token::Ident(s) | Token::QuotedIdent(s) => s,
8482 other if unreserved_keyword_text(&other).is_some() => {
8483 unreserved_keyword_text(&other).unwrap()
8484 }
8485 other => {
8486 return Err(ParseError {
8487 message: format!("expected parameter name, got {other:?}"),
8488 token_pos: self.consumed_pos(),
8489 });
8490 }
8491 };
8492 parts.push(seg);
8493 if matches!(self.peek(), Token::Dot) {
8494 self.advance();
8495 continue;
8496 }
8497 break;
8498 }
8499 Ok(parts.join(".").to_ascii_lowercase())
8500 }
8501
8502 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8503 Self::parse_set_value_inner(self)
8504 }
8505
8506 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8507 match self.advance() {
8508 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8509 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8510 Ok(crate::ast::SetValue::Default)
8511 }
8512 Token::Ident(s) | Token::QuotedIdent(s) => {
8513 let mut accum = s;
8514 while matches!(self.peek(), Token::Dot) {
8515 self.advance();
8516 let next = self.expect_ident_like()?;
8517 accum.push('.');
8518 accum.push_str(&next);
8519 }
8520 Ok(crate::ast::SetValue::Ident(accum))
8521 }
8522 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8523 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8524 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8525 // spellings that lex as keyword tokens, not idents:
8526 // `SET standard_conforming_strings = on` is in every
8527 // pg_dump preamble (`off` already lexes as an ident).
8528 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8529 // DEFAULT lexes as its keyword token, so the ident arm above
8530 // never saw it and the everyday reset form was a syntax error.
8531 Token::Default => Ok(crate::ast::SetValue::Default),
8532 // v7.40.11 — MySQL 9.7.2 accepts `NULL` here for exactly one
8533 // variable and rejects it with error 1231 for every other
8534 // (both measured); PostgreSQL 18.6 rejects the token itself
8535 // with `syntax error at or near "NULL"`. So the token is
8536 // admitted only for a MySQL session and the per-variable
8537 // decision is the executor's.
8538 Token::Null if self.mysql_dialect => Ok(crate::ast::SetValue::Null),
8539 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8540 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8541 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8542 // v7.14.0 — MySQL session/user variable RHS
8543 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8544 // Wrap as Ident so the SET handler can record it; the
8545 // engine treats `@VAR` / `@@VAR` values as opaque
8546 // strings.
8547 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8548 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8549 // is the common MySQL preamble shape. Allow a `+` or
8550 // `-` prefix on negative numerics for parity with PG
8551 // (some param defaults are negative).
8552 Token::Minus => match self.advance() {
8553 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8554 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8555 other => Err(self.err(format!(
8556 "expected numeric after `-` in SET value, got {other:?}"
8557 ))),
8558 },
8559 other => Err(self.err(format!(
8560 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8561 ))),
8562 }
8563 }
8564
8565 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8566 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8567 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8568 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8569 /// present). Modes are comma-separated per PG; SPG also
8570 /// accepts space-separated for tolerance. READ ONLY / WRITE
8571 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8572 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8573 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8574 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8575 /// session default rather than forcing READ COMMITTED.
8576 fn parse_isolation_level_clauses(
8577 &mut self,
8578 ) -> Result<crate::ast::TransactionModes, ParseError> {
8579 let mut level = IsolationLevel::default();
8580 let mut have_level = false;
8581 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8582 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8583 let mut read_only: Option<bool> = None;
8584 // v7.40.12 — DEFERRABLE was consumed and dropped, the way READ
8585 // ONLY was before v7.39. Measured on PG 18.6: `BEGIN DEFERRABLE;
8586 // SHOW transaction_deferrable` -> on; SPG answered off.
8587 let mut deferrable: Option<bool> = None;
8588 loop {
8589 // ISOLATION LEVEL …
8590 let saw_isolation =
8591 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8592 if saw_isolation {
8593 self.advance(); // ISOLATION
8594 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8595 return Err(self.err(alloc::format!(
8596 "expected LEVEL after ISOLATION, got {:?}",
8597 self.peek()
8598 )));
8599 }
8600 self.advance(); // LEVEL
8601 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8602 let w1 = self
8603 .expect_ident_like()
8604 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8605 let lc = w1.to_ascii_lowercase();
8606 level = match lc.as_str() {
8607 "serializable" => IsolationLevel::Serializable,
8608 "repeatable" => {
8609 // Expect READ
8610 let w2 = self
8611 .expect_ident_like()
8612 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8613 if !w2.eq_ignore_ascii_case("read") {
8614 return Err(self.err(alloc::format!(
8615 "expected READ after REPEATABLE, got {w2:?}"
8616 )));
8617 }
8618 IsolationLevel::RepeatableRead
8619 }
8620 "read" => {
8621 let w2 = self
8622 .expect_ident_like()
8623 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8624 match w2.to_ascii_lowercase().as_str() {
8625 "committed" => IsolationLevel::ReadCommitted,
8626 "uncommitted" => IsolationLevel::ReadUncommitted,
8627 other => {
8628 return Err(self.err(alloc::format!(
8629 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8630 )));
8631 }
8632 }
8633 }
8634 other => {
8635 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8636 }
8637 };
8638 have_level = true;
8639 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8640 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8641 // read "parsed, not behaviorally honoured", and it was
8642 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8643 // opened an ordinary read-write transaction and accepted
8644 // every write in it.
8645 self.advance();
8646 match self.peek().clone() {
8647 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8648 self.advance();
8649 read_only = Some(true);
8650 }
8651 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8652 self.advance();
8653 read_only = Some(false);
8654 }
8655 other => {
8656 return Err(self.err(alloc::format!(
8657 "expected ONLY or WRITE after READ, got {other:?}"
8658 )));
8659 }
8660 }
8661 } else if matches!(self.peek(), Token::Not) {
8662 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8663 self.advance();
8664 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8665 return Err(self.err(alloc::format!(
8666 "expected DEFERRABLE after NOT, got {:?}",
8667 self.peek()
8668 )));
8669 }
8670 self.advance();
8671 deferrable = Some(false);
8672 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8673 {
8674 self.advance();
8675 deferrable = Some(true);
8676 } else {
8677 break;
8678 }
8679 // Optional comma between modes.
8680 if matches!(self.peek(), Token::Comma) {
8681 self.advance();
8682 }
8683 }
8684 Ok(crate::ast::TransactionModes {
8685 isolation: have_level.then_some(level),
8686 read_only,
8687 deferrable,
8688 })
8689 }
8690
8691 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8692 // FOR is a v6.1.2-reserved keyword (Token::For). The
8693 // other two are bare idents — they've never needed lexer
8694 // support and we keep it that way.
8695 if !matches!(self.peek(), Token::For) {
8696 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8697 }
8698 self.advance();
8699 self.expect_keyword_ident("wal")?;
8700 self.expect_keyword_ident("position")?;
8701 let pos = self.expect_u64_literal()?;
8702 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8703 {
8704 self.advance();
8705 self.expect_keyword_ident("timeout")?;
8706 Some(self.expect_u64_literal()?)
8707 } else {
8708 None
8709 };
8710 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8711 }
8712
8713 /// v6.1.7 helper — consume a `Token::Integer` and check it
8714 /// fits `u64`. WAL positions and millisecond timeouts are
8715 /// non-negative.
8716 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8717 match self.advance() {
8718 Token::Integer(n) if n >= 0 => Ok(n as u64),
8719 Token::Integer(n) => Err(ParseError {
8720 message: format!("expected non-negative integer, got {n}"),
8721 token_pos: self.consumed_pos(),
8722 }),
8723 other => Err(ParseError {
8724 message: format!("expected integer literal, got {other:?}"),
8725 token_pos: self.consumed_pos(),
8726 }),
8727 }
8728 }
8729
8730 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8731 /// ROLE '<role>' (defaults to readonly). All string slots accept
8732 /// either a quoted ident or a quoted string literal.
8733 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8734 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8735 ///
8736 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8737 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8738 /// wire role) still parses — it is a different axis from the PG attributes.
8739 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8740 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8741 /// or RESET, so the plain attribute forms keep their old path.
8742 fn peeks_db_role_setting(&self) -> bool {
8743 let mut i = self.pos + 1; // past the object's name
8744 let word = |p: usize| -> Option<String> {
8745 match self.tokens.get(p) {
8746 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8747 Some(Token::In) => Some(String::from("in")),
8748 _ => None,
8749 }
8750 };
8751 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8752 i += 3; // IN DATABASE <name>
8753 }
8754 matches!(word(i).as_deref(), Some("set" | "reset"))
8755 }
8756
8757 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8758 use crate::ast::SetDbRoleSettingStatement;
8759 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8760 // identifier, so the ordinary name reader refuses it. Same trap
8761 // as TABLE / INDEX / FULL / DEFAULT before it.
8762 let name = if matches!(self.peek(), Token::All) {
8763 self.advance();
8764 String::from("all")
8765 } else {
8766 self.expect_ident_or_string()?
8767 };
8768 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8769 let all = name.eq_ignore_ascii_case("all");
8770 let (mut database, mut role) = if is_database {
8771 (Some(name), None)
8772 } else if all {
8773 (None, None)
8774 } else {
8775 (None, Some(name))
8776 };
8777 if matches!(self.peek(), Token::In) {
8778 self.advance();
8779 self.advance(); // DATABASE
8780 database = Some(self.expect_ident_or_string()?);
8781 }
8782 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8783 self.advance(); // SET | RESET
8784 if resetting && matches!(self.peek(), Token::All) {
8785 self.advance();
8786 self.consume_until_statement_boundary();
8787 return Ok(Statement::SetDbRoleSetting(Box::new(
8788 SetDbRoleSettingStatement {
8789 database,
8790 role,
8791 param: None,
8792 value: None,
8793 },
8794 )));
8795 }
8796 let param = self.expect_ident_like()?;
8797 let value = if resetting {
8798 None
8799 } else {
8800 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8801 // KEYWORD, so the ident-only check missed it and consumed
8802 // the word itself as the value — the same trap as ALL, one
8803 // clause over.
8804 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8805 self.advance();
8806 }
8807 Some(self.take_guc_value())
8808 };
8809 self.consume_until_statement_boundary();
8810 Ok(Statement::SetDbRoleSetting(Box::new(
8811 SetDbRoleSettingStatement {
8812 database,
8813 role,
8814 param: Some(param),
8815 value,
8816 },
8817 )))
8818 }
8819
8820 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8821 /// a quoted literal loses its quotes, a bare word or number does not.
8822 fn take_guc_value(&mut self) -> String {
8823 match self.advance() {
8824 Token::String(s) => s,
8825 Token::Integer(n) => format!("{n}"),
8826 Token::Float(f) => format!("{f}"),
8827 Token::Ident(s) | Token::QuotedIdent(s) => s,
8828 other => format!("{other:?}"),
8829 }
8830 }
8831
8832 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8833 let name = self.expect_ident_or_string()?;
8834 if self.peek_keyword_ident("with") {
8835 self.advance();
8836 }
8837 let mut password = String::new();
8838 let mut role = String::new();
8839 let mut login: Option<bool> = None;
8840 let mut inherit: Option<bool> = None;
8841 let mut superuser: Option<bool> = None;
8842 // Not a `while let`: the pattern would borrow `self` across the
8843 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8844 #[allow(clippy::while_let_loop)]
8845 loop {
8846 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8847 break;
8848 };
8849 match w.to_ascii_lowercase().as_str() {
8850 "password" => {
8851 self.advance();
8852 password = self.expect_string_literal()?;
8853 }
8854 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8855 // is the same slot.
8856 "encrypted" => {
8857 self.advance();
8858 self.expect_keyword_ident("password")?;
8859 password = self.expect_string_literal()?;
8860 }
8861 "login" => {
8862 self.advance();
8863 login = Some(true);
8864 }
8865 "nologin" => {
8866 self.advance();
8867 login = Some(false);
8868 }
8869 "inherit" => {
8870 self.advance();
8871 inherit = Some(true);
8872 }
8873 "noinherit" => {
8874 self.advance();
8875 inherit = Some(false);
8876 }
8877 "superuser" => {
8878 self.advance();
8879 superuser = Some(true);
8880 }
8881 "nosuperuser" => {
8882 self.advance();
8883 superuser = Some(false);
8884 }
8885 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8886 "role" => {
8887 self.advance();
8888 role = self.expect_string_literal()?;
8889 }
8890 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8891 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8892 // accepted and ignored so a pg_dump role block restores. They
8893 // gate capabilities SPG does not have.
8894 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8895 | "noreplication" | "bypassrls" | "nobypassrls" => {
8896 self.advance();
8897 }
8898 "connection" => {
8899 self.advance();
8900 self.expect_keyword_ident("limit")?;
8901 self.advance(); // the number
8902 }
8903 "valid" => {
8904 self.advance();
8905 self.expect_keyword_ident("until")?;
8906 self.expect_string_literal()?;
8907 }
8908 _ => break,
8909 }
8910 }
8911 if role.is_empty() {
8912 role = "readonly".to_string();
8913 }
8914 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8915 name,
8916 password,
8917 role,
8918 login,
8919 inherit,
8920 superuser,
8921 is_user,
8922 }))
8923 }
8924
8925 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8926 /// consumed the USING / WITH CHECK keyword.
8927 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8928 if !matches!(self.peek(), Token::LParen) {
8929 return Err(self.err(alloc::format!(
8930 "expected '(' after {clause}, got {:?}",
8931 self.peek()
8932 )));
8933 }
8934 self.advance();
8935 let e = self.parse_expr(0)?;
8936 if !matches!(self.peek(), Token::RParen) {
8937 return Err(self.err(alloc::format!(
8938 "expected ')' to close {clause}, got {:?}",
8939 self.peek()
8940 )));
8941 }
8942 self.advance();
8943 Ok(e)
8944 }
8945
8946 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8947 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8948 let mut roles = Vec::new();
8949 loop {
8950 roles.push(self.expect_ident_like()?);
8951 if matches!(self.peek(), Token::Comma) {
8952 self.advance();
8953 } else {
8954 break;
8955 }
8956 }
8957 Ok(roles)
8958 }
8959
8960 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8961 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8962 /// `CREATE POLICY`.
8963 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8964 use crate::ast::PolicyCmd;
8965 let name = self.expect_ident_like()?;
8966 if !matches!(self.peek(), Token::On) {
8967 return Err(self.err(alloc::format!(
8968 "expected ON after CREATE POLICY name, got {:?}",
8969 self.peek()
8970 )));
8971 }
8972 self.advance();
8973 let table = self.expect_ident_like()?;
8974
8975 let mut permissive = true;
8976 if matches!(self.peek(), Token::As) {
8977 self.advance();
8978 let w = self.expect_ident_like()?;
8979 permissive = if w.eq_ignore_ascii_case("permissive") {
8980 true
8981 } else if w.eq_ignore_ascii_case("restrictive") {
8982 false
8983 } else {
8984 return Err(self.err(alloc::format!(
8985 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8986 )));
8987 };
8988 }
8989
8990 let mut cmd = PolicyCmd::All;
8991 if matches!(self.peek(), Token::For) {
8992 self.advance();
8993 cmd = self.parse_policy_cmd()?;
8994 }
8995
8996 let mut roles = Vec::new();
8997 if matches!(self.peek(), Token::To) {
8998 self.advance();
8999 roles = self.parse_policy_roles()?;
9000 }
9001
9002 let mut using = None;
9003 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9004 {
9005 self.advance();
9006 using = Some(self.parse_paren_expr("USING")?);
9007 }
9008
9009 let mut with_check = None;
9010 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9011 {
9012 self.advance();
9013 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9014 {
9015 return Err(self.err(alloc::format!(
9016 "expected CHECK after WITH, got {:?}",
9017 self.peek()
9018 )));
9019 }
9020 self.advance();
9021 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9022 }
9023
9024 // Clause-per-command matrix (PG wording).
9025 match cmd {
9026 PolicyCmd::Insert => {
9027 if using.is_some() {
9028 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
9029 }
9030 }
9031 PolicyCmd::Select | PolicyCmd::Delete => {
9032 if with_check.is_some() {
9033 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
9034 }
9035 }
9036 PolicyCmd::Update | PolicyCmd::All => {}
9037 }
9038
9039 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
9040 name,
9041 table,
9042 permissive,
9043 cmd,
9044 roles,
9045 using,
9046 with_check,
9047 }))
9048 }
9049
9050 /// v7.39 (RLS) — the command word after `FOR`.
9051 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
9052 use crate::ast::PolicyCmd;
9053 match self.peek().clone() {
9054 Token::All => {
9055 self.advance();
9056 Ok(PolicyCmd::All)
9057 }
9058 Token::Select => {
9059 self.advance();
9060 Ok(PolicyCmd::Select)
9061 }
9062 Token::Insert => {
9063 self.advance();
9064 Ok(PolicyCmd::Insert)
9065 }
9066 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
9067 self.advance();
9068 Ok(PolicyCmd::Update)
9069 }
9070 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
9071 self.advance();
9072 Ok(PolicyCmd::Delete)
9073 }
9074 other => Err(self.err(alloc::format!(
9075 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
9076 ))),
9077 }
9078 }
9079
9080 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9081 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9082 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9083 let name = self.expect_ident_like()?;
9084 if !matches!(self.peek(), Token::On) {
9085 return Err(self.err(alloc::format!(
9086 "expected ON after ALTER POLICY name, got {:?}",
9087 self.peek()
9088 )));
9089 }
9090 self.advance();
9091 let table = self.expect_ident_like()?;
9092
9093 // RENAME TO new
9094 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9095 {
9096 self.advance();
9097 if !matches!(self.peek(), Token::To) {
9098 return Err(self.err(alloc::format!(
9099 "expected TO after RENAME, got {:?}",
9100 self.peek()
9101 )));
9102 }
9103 self.advance();
9104 let new = self.expect_ident_like()?;
9105 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9106 name,
9107 table,
9108 rename_to: Some(new),
9109 roles: None,
9110 using: None,
9111 with_check: None,
9112 }));
9113 }
9114
9115 let mut roles = None;
9116 if matches!(self.peek(), Token::To) {
9117 self.advance();
9118 roles = Some(self.parse_policy_roles()?);
9119 }
9120 let mut using = None;
9121 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9122 {
9123 self.advance();
9124 using = Some(self.parse_paren_expr("USING")?);
9125 }
9126 let mut with_check = None;
9127 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9128 {
9129 self.advance();
9130 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9131 {
9132 return Err(self.err(alloc::format!(
9133 "expected CHECK after WITH, got {:?}",
9134 self.peek()
9135 )));
9136 }
9137 self.advance();
9138 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9139 }
9140 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9141 name,
9142 table,
9143 rename_to: None,
9144 roles,
9145 using,
9146 with_check,
9147 }))
9148 }
9149
9150 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9151 /// `DROP POLICY`.
9152 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9153 let if_exists = self.consume_if_exists();
9154 let name = self.expect_ident_like()?;
9155 if !matches!(self.peek(), Token::On) {
9156 return Err(self.err(alloc::format!(
9157 "expected ON after DROP POLICY name, got {:?}",
9158 self.peek()
9159 )));
9160 }
9161 self.advance();
9162 let table = self.expect_ident_like()?;
9163 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9164 name,
9165 table,
9166 if_exists,
9167 }))
9168 }
9169}
9170fn wrap_from_leaves(
9171 e: &mut Expr,
9172 names: &[String],
9173 make: &dyn Fn(Expr) -> Expr,
9174 refs: &dyn Fn(&Expr) -> bool,
9175) {
9176 if let Expr::Column(c) = e {
9177 if c.qualifier
9178 .as_deref()
9179 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9180 {
9181 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9182 *e = make(taken);
9183 }
9184 return;
9185 }
9186 match e {
9187 Expr::Binary { lhs, rhs, .. } => {
9188 wrap_from_leaves(lhs, names, make, refs);
9189 wrap_from_leaves(rhs, names, make, refs);
9190 }
9191 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9192 wrap_from_leaves(expr, names, make, refs)
9193 }
9194 Expr::FunctionCall { args, .. } => {
9195 for a in args.iter_mut() {
9196 wrap_from_leaves(a, names, make, refs);
9197 }
9198 }
9199 Expr::Case {
9200 operand,
9201 branches,
9202 else_branch,
9203 } => {
9204 if let Some(o) = operand.as_deref_mut() {
9205 wrap_from_leaves(o, names, make, refs);
9206 }
9207 for (w, t) in branches.iter_mut() {
9208 wrap_from_leaves(w, names, make, refs);
9209 wrap_from_leaves(t, names, make, refs);
9210 }
9211 if let Some(el) = else_branch.as_deref_mut() {
9212 wrap_from_leaves(el, names, make, refs);
9213 }
9214 }
9215 // Compound variants the walk doesn't decompose: keep the
9216 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9217 // a source table, so nothing regresses.
9218 other => {
9219 if refs(other) {
9220 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9221 *other = make(taken);
9222 }
9223 }
9224 }
9225}
9226
9227/// v7.39 (round 241) — does this expression reference any of the FROM /
9228/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9229/// lowerings)?
9230fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9231 match e {
9232 Expr::Column(c) => c
9233 .qualifier
9234 .as_deref()
9235 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9236 Expr::Binary { lhs, rhs, .. } => {
9237 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9238 }
9239 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9240 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9241 Expr::Case {
9242 operand,
9243 branches,
9244 else_branch,
9245 } => {
9246 operand
9247 .as_deref()
9248 .is_some_and(|o| expr_refs_tables(o, names))
9249 || branches
9250 .iter()
9251 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9252 || else_branch
9253 .as_deref()
9254 .is_some_and(|el| expr_refs_tables(el, names))
9255 }
9256 _ => false,
9257 }
9258}
9259
9260impl Parser {
9261 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9262 /// Caller already consumed the leading `UPDATE` ident.
9263 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9264 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9265 /// after the target name has been read. `JOIN` is a reserved token;
9266 /// the qualifiers are bare idents.
9267 fn peek_is_update_join_start(&self) -> bool {
9268 match self.peek() {
9269 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9270 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9271 Token::Join
9272 | Token::Inner
9273 | Token::Left
9274 | Token::Right
9275 | Token::Cross
9276 | Token::Full => true,
9277 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9278 Token::Ident(s) | Token::QuotedIdent(s) => {
9279 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9280 }
9281 _ => false,
9282 }
9283 }
9284
9285 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9286 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9287 /// expression on the right, and `:=` as a second spelling of `=`.
9288 ///
9289 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9290 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9291 /// and holding this loop's `Vec` + `String` locals there overflowed the
9292 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9293 #[inline(never)]
9294 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9295 let mut assigns: Vec<(String, Expr)> = Vec::new();
9296 let mut settings: Vec<(String, Expr)> = Vec::new();
9297 loop {
9298 // v7.39 (round 554) — a plain NAME here is a session
9299 // setting, not a user variable. mysqldump writes the two in
9300 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9301 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9302 // changes it — and this refused the mixture outright, so no
9303 // dump could be restored past its preamble.
9304 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9305 self.advance();
9306 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9307 return Err(self.err(alloc::format!(
9308 "expected `=` after {name}, got {:?}",
9309 self.peek()
9310 )));
9311 }
9312 self.advance();
9313 let value = self.parse_expr(0)?;
9314 settings.push((name.to_ascii_lowercase(), value));
9315 if matches!(self.peek(), Token::Comma) {
9316 self.advance();
9317 continue;
9318 }
9319 break;
9320 }
9321 let Token::SessionVar(raw) = self.peek().clone() else {
9322 return Err(self.err(alloc::format!(
9323 "expected a user variable after SET, got {:?}",
9324 self.peek()
9325 )));
9326 };
9327 if raw.starts_with("@@") {
9328 return Err(self.err(alloc::string::String::from(
9329 "cannot mix `@@` settings with `@` user variables in one SET",
9330 )));
9331 }
9332 self.advance();
9333 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9334 return Err(self.err(alloc::format!(
9335 "expected `=` or `:=` after {raw}, got {:?}",
9336 self.peek()
9337 )));
9338 }
9339 self.advance();
9340 let value = self.parse_expr(0)?;
9341 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9342 if matches!(self.peek(), Token::Comma) {
9343 self.advance();
9344 continue;
9345 }
9346 break;
9347 }
9348 Ok(Statement::SetUserVars(assigns, settings))
9349 }
9350
9351 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9352 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9353 // NAMED `only` until now, which failed on `relation "only" does
9354 // not exist`. The lookahead is what keeps a table actually
9355 // called `only` working: the keyword is only a keyword when a
9356 // TABLE NAME follows it — and `SET` arrives as an identifier
9357 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9358 // for the table and die on the `=`. Measured by the pin.
9359 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9360 if s.eq_ignore_ascii_case("only"))
9361 && matches!(
9362 self.tokens.get(self.pos + 1),
9363 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9364 );
9365 if only {
9366 self.advance();
9367 }
9368 let table = self.expect_ident_like()?;
9369 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9370 // bare spelling; a bare identifier that is the SET keyword itself
9371 // is the clause, not an alias.
9372 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9373 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9374 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9375 // following JOIN a syntax error.
9376 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9377 let alias = if matches!(self.peek(), Token::As) {
9378 self.advance();
9379 Some(self.expect_ident_like()?)
9380 } else {
9381 match self.peek() {
9382 Token::Ident(s) | Token::QuotedIdent(s)
9383 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9384 {
9385 let a = s.clone();
9386 self.advance();
9387 Some(a)
9388 }
9389 _ => None,
9390 }
9391 };
9392 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9393 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9394 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9395 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9396 // The FIRST table is the mutation target and the rest are sources —
9397 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9398 // SPG already lowers onto correlated subqueries. So rewind, let
9399 // `parse_from_clause` read the whole list (it handles aliases, comma
9400 // lists, and every JOIN form), then peel the target off the front.
9401 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9402 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9403 {
9404 // NOTE: `advance()` destroys the tokens it returns
9405 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9406 // is NOT possible — the tail is read forward, once, through the
9407 // same grammar `parse_from_clause` uses after its primary.
9408 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9409 let mut joins = self.parse_from_joins(&target_qual)?;
9410 if joins.is_empty() {
9411 return Err(self.err(alloc::string::String::from(
9412 "multi-table UPDATE needs at least one source table",
9413 )));
9414 }
9415 let head = joins.remove(0);
9416 // A LEFT join keeps every target row (the unmatched ones see NULL
9417 // on the source side), so it must NOT get the EXISTS row filter
9418 // the inner / comma forms use.
9419 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9420 let src = FromClause {
9421 primary: head.table,
9422 joins,
9423 };
9424 (Some(src), head.on, outer)
9425 } else {
9426 (None, None, false)
9427 };
9428 self.expect_keyword_ident("set")?;
9429 let mut assignments = Vec::new();
9430 loop {
9431 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9432 // …)` — the parenthesized multi-assignment. Expressions
9433 // assign positionally; a subquery RHS clones per column
9434 // keeping only the Nth projection item.
9435 if matches!(self.peek(), Token::LParen) {
9436 self.advance();
9437 let mut cols = alloc::vec![self.expect_ident_like()?];
9438 while matches!(self.peek(), Token::Comma) {
9439 self.advance();
9440 cols.push(self.expect_ident_like()?);
9441 }
9442 if !matches!(self.peek(), Token::RParen) {
9443 return Err(self.err(format!(
9444 "expected ')' after SET column list, got {:?}",
9445 self.peek()
9446 )));
9447 }
9448 self.advance();
9449 if !matches!(self.peek(), Token::Eq) {
9450 return Err(self.err(format!(
9451 "expected `=` after SET column list, got {:?}",
9452 self.peek()
9453 )));
9454 }
9455 self.advance();
9456 if !matches!(self.peek(), Token::LParen) {
9457 return Err(self.err(format!(
9458 "expected '(' after SET (…) =, got {:?}",
9459 self.peek()
9460 )));
9461 }
9462 self.advance();
9463 if matches!(self.peek(), Token::Select) {
9464 let inner = match self.parse_select_stmt()? {
9465 Statement::Select(s) => s,
9466 other => {
9467 return Err(self.err(alloc::format!(
9468 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9469 )));
9470 }
9471 };
9472 if !matches!(self.peek(), Token::RParen) {
9473 return Err(self.err(format!(
9474 "expected ')' after SET subquery, got {:?}",
9475 self.peek()
9476 )));
9477 }
9478 self.advance();
9479 if inner.items.len() != cols.len() {
9480 return Err(self.err(alloc::format!(
9481 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9482 cols.len(),
9483 inner.items.len()
9484 )));
9485 }
9486 for (i, col) in cols.into_iter().enumerate() {
9487 let mut sub = inner.clone();
9488 sub.items = alloc::vec![sub.items[i].clone()];
9489 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9490 }
9491 } else {
9492 let mut exprs = alloc::vec![self.parse_expr(0)?];
9493 while matches!(self.peek(), Token::Comma) {
9494 self.advance();
9495 exprs.push(self.parse_expr(0)?);
9496 }
9497 if !matches!(self.peek(), Token::RParen) {
9498 return Err(self.err(format!(
9499 "expected ')' after SET row values, got {:?}",
9500 self.peek()
9501 )));
9502 }
9503 self.advance();
9504 if exprs.len() != cols.len() {
9505 return Err(self.err(alloc::format!(
9506 "SET (…) = (…) arity mismatch: {} columns, {} values",
9507 cols.len(),
9508 exprs.len()
9509 )));
9510 }
9511 for (col, e) in cols.into_iter().zip(exprs) {
9512 assignments.push((col, e));
9513 }
9514 }
9515 if matches!(self.peek(), Token::Comma) {
9516 self.advance();
9517 continue;
9518 }
9519 break;
9520 }
9521 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9522 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9523 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9524 // `public.` dump qualifiers), so the qualifier has to be read off
9525 // the token stream first — otherwise `SET b.v = 888` would write
9526 // to the TARGET table's `v` while naming a source table, a
9527 // silent-wrong. A qualifier naming a SOURCE table means a
9528 // multi-TARGET update — mutating two tables in one statement —
9529 // which SPG does not model, so it is refused loudly.
9530 let set_qual: Option<String> = if mysql_from.is_some()
9531 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9532 {
9533 match self.peek() {
9534 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9535 _ => None,
9536 }
9537 } else {
9538 None
9539 };
9540 let col = self.expect_ident_like()?;
9541 if let Some(q) = set_qual {
9542 let names_target = q.eq_ignore_ascii_case(&table)
9543 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9544 if !names_target {
9545 return Err(self.err(alloc::format!(
9546 "multi-table UPDATE can only assign to its first table \
9547 ({table}); `{q}.{col}` targets another table"
9548 )));
9549 }
9550 }
9551 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9552 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9553 // `__column_default` marker lowering just below). PG assigns to the
9554 // i-th (1-based) element, NULL-padding when i exceeds the length.
9555 if matches!(self.peek(), Token::LBracket) {
9556 self.advance();
9557 let index = self.parse_expr(0)?;
9558 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9559 // (and the open `arr[lo:]`), lowered to
9560 // `__array_assign_slice`. Only the single-subscript form
9561 // parsed before, so a slice assignment was a syntax error.
9562 let mut slice_hi: Option<Option<Expr>> = None;
9563 if matches!(self.peek(), Token::Colon) {
9564 self.advance();
9565 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9566 None
9567 } else {
9568 Some(self.parse_expr(0)?)
9569 });
9570 }
9571 if !matches!(self.peek(), Token::RBracket) {
9572 return Err(self.err(format!(
9573 "expected `]` after array subscript in UPDATE SET, got {:?}",
9574 self.peek()
9575 )));
9576 }
9577 self.advance();
9578 if !matches!(self.peek(), Token::Eq) {
9579 return Err(self.err(format!(
9580 "expected `=` after array subscript in UPDATE SET, got {:?}",
9581 self.peek()
9582 )));
9583 }
9584 self.advance();
9585 let value = self.parse_expr(0)?;
9586 // PG merges several subscript writes to the same column into one
9587 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9588 // assignment to `col` rather than each overwriting the original.
9589 let existing = assignments.iter().position(|(c, _)| c == &col);
9590 let base = match existing {
9591 Some(i) => assignments[i].1.clone(),
9592 None => Expr::Column(ColumnName {
9593 qualifier: None,
9594 name: col.clone(),
9595 }),
9596 };
9597 let assigned = match slice_hi {
9598 None => Expr::FunctionCall {
9599 name: "__array_assign".to_string(),
9600 args: alloc::vec![base, index, value],
9601 },
9602 Some(hi) => Expr::FunctionCall {
9603 name: "__array_assign_slice".to_string(),
9604 args: alloc::vec![
9605 base,
9606 index,
9607 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9608 value,
9609 ],
9610 },
9611 };
9612 match existing {
9613 Some(i) => assignments[i].1 = assigned,
9614 None => assignments.push((col, assigned)),
9615 }
9616 if matches!(self.peek(), Token::Comma) {
9617 self.advance();
9618 continue;
9619 }
9620 break;
9621 }
9622 if !matches!(self.peek(), Token::Eq) {
9623 return Err(self.err(format!(
9624 "expected `=` after column name in UPDATE SET, got {:?}",
9625 self.peek()
9626 )));
9627 }
9628 self.advance();
9629 // `SET col = DEFAULT` — the column's declared default;
9630 // rides out as a marker call the update executor
9631 // resolves against the schema.
9632 let value = if matches!(self.peek(), Token::Default) {
9633 self.advance();
9634 Expr::FunctionCall {
9635 name: "__column_default".to_string(),
9636 args: Vec::new(),
9637 }
9638 } else {
9639 self.parse_expr(0)?
9640 };
9641 assignments.push((col, value));
9642 if matches!(self.peek(), Token::Comma) {
9643 self.advance();
9644 continue;
9645 }
9646 break;
9647 }
9648 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9649 // update. Lowers onto the correlated-subquery machinery:
9650 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9651 // and each assignment that references a FROM-list table
9652 // wraps into a correlated scalar subquery
9653 // (SELECT expr FROM src WHERE cond). Equivalent for the
9654 // unique-join shape (the overwhelmingly common one); a
9655 // multi-match, which PG resolves by arbitrary pick,
9656 // surfaces as a scalar-subquery cardinality error instead
9657 // of a silent arbitrary result.
9658 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9659 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9660 // the SAME lowering below. Both spellings together is not legal in
9661 // either dialect.
9662 let from_clause = if let Some(fc) = mysql_from {
9663 if matches!(self.peek(), Token::From) {
9664 return Err(self.err(alloc::string::String::from(
9665 "multi-table UPDATE already names its sources; drop the FROM clause",
9666 )));
9667 }
9668 Some(fc)
9669 } else if matches!(self.peek(), Token::From) {
9670 self.advance();
9671 Some(self.parse_from_clause()?)
9672 } else {
9673 None
9674 };
9675 let where_ = if matches!(self.peek(), Token::Where) {
9676 self.advance();
9677 Some(self.parse_expr(0)?)
9678 } else {
9679 None
9680 };
9681 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9682 // and the TARGET-row filter are NOT the same predicate once a LEFT
9683 // join is involved:
9684 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9685 // one conjunction, and the whole thing filters target rows via
9686 // EXISTS.
9687 // * LEFT join: only the ON predicate belongs inside the source
9688 // subquery. The WHERE still filters TARGET rows (with source
9689 // columns read through the correlated subquery, which yields NULL
9690 // for an unmatched row — exactly LEFT-join semantics).
9691 // Round 420 folded ON into WHERE unconditionally and then dropped the
9692 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9693 // WHERE a.id > 1` updated EVERY row.
9694 let sub_where = match (mysql_on.clone(), where_.clone()) {
9695 _ if mysql_outer => mysql_on.clone(),
9696 (Some(on), Some(w)) => Some(Expr::Binary {
9697 lhs: Box::new(on),
9698 op: crate::ast::BinOp::And,
9699 rhs: Box::new(w),
9700 }),
9701 (Some(on), None) => Some(on),
9702 (None, w) => w,
9703 };
9704 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9705 // has no such clause on UPDATE, so this is accepted only under the
9706 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9707 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9708 let mut returning = self.parse_optional_returning()?;
9709 // v7.39 (round 533) — kept for the engine, which can resolve the
9710 // UNQUALIFIED leaves this lowering has to leave alone.
9711 let from_sources = from_clause.as_ref().map(|fc| {
9712 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9713 from: fc.clone(),
9714 sub_where: sub_where.clone(),
9715 })
9716 });
9717 let (assignments, where_) = if let Some(fc) = from_clause {
9718 let names: Vec<String> = core::iter::once(&fc.primary)
9719 .chain(fc.joins.iter().map(|j| &j.table))
9720 .flat_map(|t| {
9721 t.alias
9722 .clone()
9723 .into_iter()
9724 .chain(core::iter::once(t.name.clone()))
9725 })
9726 .collect();
9727 let refs_list = |e: &Expr| -> bool {
9728 fn walk(e: &Expr, names: &[String]) -> bool {
9729 match e {
9730 Expr::Column(c) => c
9731 .qualifier
9732 .as_deref()
9733 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9734 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9735 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9736 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9737 Expr::Case {
9738 operand,
9739 branches,
9740 else_branch,
9741 } => {
9742 operand.as_deref().is_some_and(|o| walk(o, names))
9743 || branches
9744 .iter()
9745 .any(|(w, t)| walk(w, names) || walk(t, names))
9746 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9747 }
9748 _ => false,
9749 }
9750 }
9751 walk(e, &names)
9752 };
9753 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9754 locking: None,
9755 ctes: Vec::new(),
9756 distinct: false,
9757 distinct_on: Vec::new(),
9758 items,
9759 from: Some(fc.clone()),
9760 where_: sub_where.clone(),
9761 group_by: None,
9762 group_by_all: false,
9763 having: None,
9764 unions: Vec::new(),
9765 order_by: Vec::new(),
9766 limit: None,
9767 offset: None,
9768 limit_with_ties: false,
9769 window_check_exprs: Vec::new(),
9770 };
9771 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9772 // assignment RHS with a correlated scalar subquery, instead of
9773 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9774 // column reference (`SET v = v + u.bonus`, where `v` is the target
9775 // table's column) inside a subquery whose FROM only has the source
9776 // table, so the unqualified `v` resolved against the source and
9777 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9778 // context — where they belong — fixes it; only the source columns
9779 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9780 // compound variants the leaf-walk doesn't decompose.
9781 let make_subq = |inner: Expr| {
9782 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9783 expr: inner,
9784 alias: None,
9785 }])))
9786 };
9787 let assignments = assignments
9788 .into_iter()
9789 .map(|(col, mut expr)| {
9790 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9791 (col, expr)
9792 })
9793 .collect();
9794 let exists = Expr::Exists {
9795 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9796 expr: Expr::Literal(Literal::Integer(1)),
9797 alias: None,
9798 }])),
9799 negated: false,
9800 };
9801 // v7.39 (round 241) — RETURNING may reference the FROM-list
9802 // tables too (`RETURNING emp.id, dept.name`); the same
9803 // leaf-to-correlated-subquery lowering the assignments get.
9804 // Without it the qualifier died at eval with "unknown table
9805 // qualifier". (RETURNING was parsed before this block — the
9806 // lowering is a pure AST transformation.)
9807 if let Some(items) = returning.as_mut() {
9808 for item in items.iter_mut() {
9809 if let SelectItem::Expr { expr, .. } = item {
9810 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9811 }
9812 }
9813 }
9814 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9815 // EVERY matching target row: it gets no EXISTS filter, but the
9816 // caller's WHERE still applies, with source columns read through
9817 // the correlated subquery (NULL when unmatched — LEFT-join
9818 // semantics). `sub_where` above already excluded the WHERE from
9819 // the source subquery for this case.
9820 if mysql_outer {
9821 let mut outer = where_;
9822 if let Some(w) = outer.as_mut() {
9823 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9824 }
9825 (assignments, outer)
9826 } else {
9827 (assignments, Some(exists))
9828 }
9829 } else {
9830 (assignments, where_)
9831 };
9832 Ok(Statement::Update(crate::ast::UpdateStatement {
9833 ctes: Vec::new(),
9834 table,
9835 only,
9836 alias,
9837 assignments,
9838 from_sources,
9839 where_,
9840 order_limit: update_order_limit,
9841 returning,
9842 }))
9843 }
9844
9845 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9846 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9847 /// clause and its meaning are identical, so both call this rather than
9848 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9849 /// legal. PG has no such clause on either statement, so it is read only
9850 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9851 /// errors.
9852 ///
9853 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9854 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9855 /// stack in round 430.
9856 #[inline(never)]
9857 fn parse_mysql_dml_order_limit(
9858 &mut self,
9859 what: &str,
9860 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9861 if !self.mysql_dialect {
9862 return Ok(None);
9863 }
9864 let order_by = self.parse_order_by_keys()?;
9865 let limit = if matches!(self.peek(), Token::Limit) {
9866 self.advance();
9867 let tok = self.advance();
9868 let Token::Integer(n) = tok else {
9869 return Err(self.err(alloc::format!(
9870 "expected integer after {what} LIMIT, got {tok:?}"
9871 )));
9872 };
9873 // MySQL rejects the `LIMIT offset, count` form here — only a
9874 // single row count is legal on a DML statement.
9875 if matches!(self.peek(), Token::Comma) {
9876 return Err(self.err(alloc::format!(
9877 "{what} LIMIT takes a row count, not an offset"
9878 )));
9879 }
9880 let n = u32::try_from(n)
9881 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9882 Some(n)
9883 } else {
9884 None
9885 };
9886 if order_by.is_empty() && limit.is_none() {
9887 return Ok(None);
9888 }
9889 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9890 order_by,
9891 limit,
9892 })))
9893 }
9894
9895 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9896 /// the leading `DELETE` ident.
9897 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9898 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9899 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9900 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9901 // parse here; it reaches the existing USING path with the target
9902 // repeated in the list, which the source-list peel below handles.)
9903 // More than one name is a multi-TARGET delete, which SPG does not
9904 // model; it is refused rather than half-applied.
9905 let mysql_pre_target: Option<String> =
9906 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9907 let first = self.expect_ident_like()?;
9908 if matches!(self.peek(), Token::Comma) {
9909 return Err(self.err(alloc::format!(
9910 "multi-table DELETE can only delete from one table; \
9911 `DELETE {first}, …` names several"
9912 )));
9913 }
9914 Some(first)
9915 } else {
9916 None
9917 };
9918 if !matches!(self.peek(), Token::From) {
9919 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9920 }
9921 self.advance();
9922 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9923 // lookahead as the UPDATE spelling.
9924 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9925 if s.eq_ignore_ascii_case("only"))
9926 && matches!(
9927 self.tokens.get(self.pos + 1),
9928 Some(Token::Ident(_) | Token::QuotedIdent(_))
9929 );
9930 if only {
9931 self.advance();
9932 }
9933 let table = self.expect_ident_like()?;
9934 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9935 // spelling must not swallow the clause keywords that can follow
9936 // the target.
9937 let alias = if matches!(self.peek(), Token::As) {
9938 self.advance();
9939 Some(self.expect_ident_like()?)
9940 } else {
9941 match self.peek() {
9942 Token::Ident(s) | Token::QuotedIdent(s)
9943 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9944 {
9945 let a = s.clone();
9946 self.advance();
9947 Some(a)
9948 }
9949 _ => None,
9950 }
9951 };
9952 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9953 // through the SAME join grammar the FROM clause uses (see the
9954 // `advance()`-destroys-tokens note on `parse_from_joins`).
9955 let mut mysql_on: Option<Expr> = None;
9956 let mut mysql_outer = false;
9957 let mysql_using = if mysql_pre_target.is_some()
9958 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9959 {
9960 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9961 let mut joins = self.parse_from_joins(&target_qual)?;
9962 if joins.is_empty() {
9963 return Err(self.err(alloc::string::String::from(
9964 "multi-table DELETE needs at least one source table",
9965 )));
9966 }
9967 let head = joins.remove(0);
9968 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9969 mysql_on = head.on;
9970 Some(FromClause {
9971 primary: head.table,
9972 joins,
9973 })
9974 } else {
9975 None
9976 };
9977 // The pre-FROM target must be the table the FROM names (or its
9978 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9979 // is not the scan target.
9980 if let Some(t) = &mysql_pre_target {
9981 let names_target = t.eq_ignore_ascii_case(&table)
9982 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9983 if !names_target {
9984 return Err(self.err(alloc::format!(
9985 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9986 )));
9987 }
9988 }
9989 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9990 // delete. Same lowering as UPDATE … FROM: the WHERE
9991 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9992 // target row by the correlated machinery.
9993 let using_clause = if let Some(fc) = mysql_using {
9994 Some(fc)
9995 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9996 self.advance();
9997 let mut fc = self.parse_from_clause()?;
9998 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9999 // repeats the TARGET as the first USING entry (PG's spelling
10000 // lists only the extra sources). Peel it so the source subquery
10001 // does not re-scan — and shadow — the target table.
10002 let primary_is_target =
10003 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
10004 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
10005 let head = fc.joins.remove(0);
10006 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
10007 mysql_on = head.on;
10008 fc = FromClause {
10009 primary: head.table,
10010 joins: fc.joins,
10011 };
10012 }
10013 Some(fc)
10014 } else {
10015 None
10016 };
10017 let where_ = if matches!(self.peek(), Token::Where) {
10018 self.advance();
10019 Some(self.parse_expr(0)?)
10020 } else {
10021 None
10022 };
10023 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
10024 // read before RETURNING (MariaDB's own extension trails the LIMIT).
10025 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
10026 let mut returning = self.parse_optional_returning()?;
10027 let where_ = if let Some(fc) = using_clause {
10028 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
10029 // a USING-table reference in RETURNING becomes a correlated
10030 // scalar subquery over the USING list.
10031 let names: Vec<String> = core::iter::once(&fc.primary)
10032 .chain(fc.joins.iter().map(|j| &j.table))
10033 .flat_map(|t| {
10034 t.alias
10035 .clone()
10036 .into_iter()
10037 .chain(core::iter::once(t.name.clone()))
10038 })
10039 .collect();
10040 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
10041 // join filters the SOURCE subquery on the ON predicate alone and
10042 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
10043 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
10044 // rows); every other form folds ON and WHERE into one EXISTS.
10045 let sub_where = match (mysql_on.clone(), where_.clone()) {
10046 _ if mysql_outer => mysql_on.clone(),
10047 (Some(on), Some(w)) => Some(Expr::Binary {
10048 lhs: Box::new(on),
10049 op: crate::ast::BinOp::And,
10050 rhs: Box::new(w),
10051 }),
10052 (Some(on), None) => Some(on),
10053 (None, w) => w,
10054 };
10055 let exists_where = sub_where.clone();
10056 let sub_fc = fc.clone();
10057 let make_subq = move |leaf: Expr| -> Expr {
10058 Expr::ScalarSubquery(Box::new(SelectStatement {
10059 locking: None,
10060 ctes: Vec::new(),
10061 distinct: false,
10062 distinct_on: Vec::new(),
10063 items: alloc::vec![SelectItem::Expr {
10064 expr: leaf,
10065 alias: None,
10066 }],
10067 from: Some(sub_fc.clone()),
10068 where_: sub_where.clone(),
10069 group_by: None,
10070 group_by_all: false,
10071 having: None,
10072 unions: Vec::new(),
10073 order_by: Vec::new(),
10074 limit: None,
10075 offset: None,
10076 limit_with_ties: false,
10077 window_check_exprs: Vec::new(),
10078 }))
10079 };
10080 let refs = |e: &Expr| expr_refs_tables(e, &names);
10081 if let Some(items) = returning.as_mut() {
10082 for item in items.iter_mut() {
10083 if let SelectItem::Expr { expr, .. } = item {
10084 wrap_from_leaves(expr, &names, &make_subq, &refs);
10085 }
10086 }
10087 }
10088 // A LEFT join deletes the target rows the WHERE selects, reading
10089 // source columns through the correlated subquery (NULL when
10090 // unmatched); no EXISTS row filter.
10091 if mysql_outer {
10092 let mut outer = where_;
10093 if let Some(w) = outer.as_mut() {
10094 wrap_from_leaves(w, &names, &make_subq, &refs);
10095 }
10096 outer
10097 } else {
10098 Some(Expr::Exists {
10099 subquery: Box::new(SelectStatement {
10100 locking: None,
10101 ctes: Vec::new(),
10102 distinct: false,
10103 distinct_on: Vec::new(),
10104 items: alloc::vec![SelectItem::Expr {
10105 expr: Expr::Literal(Literal::Integer(1)),
10106 alias: None,
10107 }],
10108 from: Some(fc),
10109 where_: exists_where,
10110 group_by: None,
10111 group_by_all: false,
10112 having: None,
10113 unions: Vec::new(),
10114 order_by: Vec::new(),
10115 limit: None,
10116 offset: None,
10117 limit_with_ties: false,
10118 window_check_exprs: Vec::new(),
10119 }),
10120 negated: false,
10121 })
10122 }
10123 } else {
10124 where_
10125 };
10126 Ok(Statement::Delete(crate::ast::DeleteStatement {
10127 ctes: Vec::new(),
10128 table,
10129 only,
10130 alias,
10131 where_,
10132 order_limit: delete_order_limit,
10133 returning,
10134 }))
10135 }
10136
10137 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10138 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10139 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10140 /// keyword. v7.17 surface:
10141 /// * source: table reference (subquery source is a follow-up)
10142 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10143 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10144 /// * AND-conditioned WHEN clauses; clauses tried in declaration
10145 /// order
10146 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10147 // INTO
10148 let is_into_kw = matches!(self.peek(), Token::Into)
10149 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10150 if !is_into_kw {
10151 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10152 }
10153 self.advance();
10154 let target = self.expect_ident_like()?;
10155 // Optional alias — bare ident before USING.
10156 let target_alias = match self.peek() {
10157 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10158 Some(self.expect_ident_like()?)
10159 }
10160 _ => None,
10161 };
10162 // USING
10163 let is_using_kw = matches!(
10164 self.peek(),
10165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10166 );
10167 if !is_using_kw {
10168 return Err(self.err(format!(
10169 "expected USING after MERGE INTO target, got {:?}",
10170 self.peek()
10171 )));
10172 }
10173 self.advance();
10174 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10175 // <table> [alias]`. PG requires an alias after a subquery source.
10176 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10177 self.advance(); // (
10178 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10179 // constant-SELECT lowering the derived-table parser uses
10180 // (PG deletes through this form; it was a parse error).
10181 let inner = if matches!(self.peek(), Token::Values) {
10182 self.advance(); // VALUES
10183 Statement::Select(self.parse_values_rows_body()?)
10184 } else {
10185 self.parse_select_stmt()?
10186 };
10187 match self.advance() {
10188 Token::RParen => {}
10189 other => {
10190 return Err(self.err(format!(
10191 "expected ')' after MERGE USING subquery, got {other:?}"
10192 )));
10193 }
10194 }
10195 let Statement::Select(sub) = inner else {
10196 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10197 };
10198 (String::new(), Some(Box::new(sub)))
10199 } else {
10200 (self.expect_ident_like()?, None)
10201 };
10202 let source_alias = match self.peek() {
10203 Token::Ident(s) | Token::QuotedIdent(s)
10204 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10205 {
10206 Some(self.expect_ident_like()?)
10207 }
10208 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10209 self.advance(); // AS
10210 Some(self.expect_ident_like()?)
10211 }
10212 _ => None,
10213 };
10214 // v7.39 (round 768, F31-D5) — optional positional column-alias
10215 // list after the source alias (`s(id, v)`).
10216 let mut source_column_aliases: Vec<String> = Vec::new();
10217 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10218 self.advance();
10219 loop {
10220 source_column_aliases.push(self.expect_ident_like()?);
10221 match self.peek() {
10222 Token::Comma => {
10223 self.advance();
10224 }
10225 Token::RParen => {
10226 self.advance();
10227 break;
10228 }
10229 other => {
10230 return Err(self.err(format!(
10231 "expected ',' or ')' in MERGE source column list, got {other:?}"
10232 )));
10233 }
10234 }
10235 }
10236 }
10237 if source_select.is_some() && source_alias.is_none() {
10238 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10239 }
10240 // ON
10241 if !matches!(self.peek(), Token::On) {
10242 return Err(self.err(format!(
10243 "expected ON after MERGE … USING source, got {:?}",
10244 self.peek()
10245 )));
10246 }
10247 self.advance();
10248 let on = self.parse_expr(0)?;
10249 // One or more WHEN clauses.
10250 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10251 loop {
10252 let is_when_kw = matches!(
10253 self.peek(),
10254 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10255 );
10256 if !is_when_kw {
10257 break;
10258 }
10259 self.advance(); // WHEN
10260 // [NOT] MATCHED
10261 let matched = if matches!(self.peek(), Token::Not) {
10262 self.advance();
10263 crate::ast::MergeMatched::NotMatched
10264 } else {
10265 crate::ast::MergeMatched::Matched
10266 };
10267 let is_matched_kw = matches!(
10268 self.peek(),
10269 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10270 );
10271 if !is_matched_kw {
10272 return Err(self.err(format!(
10273 "expected MATCHED in WHEN clause, got {:?}",
10274 self.peek()
10275 )));
10276 }
10277 self.advance();
10278 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10279 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10280 // to fire for target rows no source row matches.
10281 let mut matched = matched;
10282 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10283 self.advance();
10284 match self.peek() {
10285 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10286 self.advance();
10287 matched = crate::ast::MergeMatched::NotMatchedBySource;
10288 }
10289 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10290 self.advance();
10291 }
10292 other => {
10293 return Err(self.err(format!(
10294 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10295 )));
10296 }
10297 }
10298 }
10299 // Optional AND <expr>
10300 let condition = if matches!(self.peek(), Token::And) {
10301 self.advance();
10302 Some(self.parse_expr(0)?)
10303 } else {
10304 None
10305 };
10306 // THEN
10307 let is_then_kw = matches!(
10308 self.peek(),
10309 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10310 );
10311 if !is_then_kw {
10312 return Err(self.err(format!(
10313 "expected THEN in WHEN clause, got {:?}",
10314 self.peek()
10315 )));
10316 }
10317 self.advance();
10318 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10319 let action = match self.peek().clone() {
10320 Token::Insert => {
10321 self.advance();
10322 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10323 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10324 // VALUES (…)` omits it and fills every column in declaration
10325 // order. PG accepts this; SPG used to require the list.
10326 let mut columns: Vec<String> = Vec::new();
10327 if matches!(self.peek(), Token::LParen) {
10328 self.advance();
10329 loop {
10330 columns.push(self.expect_ident_like()?);
10331 if matches!(self.peek(), Token::Comma) {
10332 self.advance();
10333 continue;
10334 }
10335 break;
10336 }
10337 if !matches!(self.peek(), Token::RParen) {
10338 return Err(self.err(format!(
10339 "expected ')' after INSERT column list, got {:?}",
10340 self.peek()
10341 )));
10342 }
10343 self.advance();
10344 }
10345 // VALUES (...)
10346 if !matches!(self.peek(), Token::Values) {
10347 return Err(self.err(format!(
10348 "expected VALUES in MERGE INSERT, got {:?}",
10349 self.peek()
10350 )));
10351 }
10352 self.advance();
10353 if !matches!(self.peek(), Token::LParen) {
10354 return Err(self.err(format!(
10355 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10356 self.peek()
10357 )));
10358 }
10359 self.advance();
10360 let mut values: Vec<crate::ast::Expr> = Vec::new();
10361 loop {
10362 values.push(self.parse_expr(0)?);
10363 if matches!(self.peek(), Token::Comma) {
10364 self.advance();
10365 continue;
10366 }
10367 break;
10368 }
10369 if !matches!(self.peek(), Token::RParen) {
10370 return Err(self.err(format!(
10371 "expected ')' after MERGE INSERT values, got {:?}",
10372 self.peek()
10373 )));
10374 }
10375 self.advance();
10376 // Empty column list = positional into every column, so the
10377 // count is checked against the table arity at execution.
10378 if !columns.is_empty() && columns.len() != values.len() {
10379 return Err(self.err(format!(
10380 "MERGE INSERT column count ({}) ≠ value count ({})",
10381 columns.len(),
10382 values.len()
10383 )));
10384 }
10385 crate::ast::MergeAction::Insert { columns, values }
10386 }
10387 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10388 self.advance();
10389 // SET
10390 let is_set_kw = matches!(
10391 self.peek(),
10392 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10393 );
10394 if !is_set_kw {
10395 return Err(self.err(format!(
10396 "expected SET after UPDATE in MERGE, got {:?}",
10397 self.peek()
10398 )));
10399 }
10400 self.advance();
10401 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10402 loop {
10403 let col = self.expect_ident_like()?;
10404 if !matches!(self.peek(), Token::Eq) {
10405 return Err(self.err(format!(
10406 "expected '=' in MERGE UPDATE assignment, got {:?}",
10407 self.peek()
10408 )));
10409 }
10410 self.advance();
10411 let expr = self.parse_expr(0)?;
10412 assignments.push((col, expr));
10413 if matches!(self.peek(), Token::Comma) {
10414 self.advance();
10415 continue;
10416 }
10417 break;
10418 }
10419 crate::ast::MergeAction::Update { assignments }
10420 }
10421 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10422 self.advance();
10423 crate::ast::MergeAction::Delete
10424 }
10425 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10426 self.advance();
10427 let is_nothing_kw = matches!(
10428 self.peek(),
10429 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10430 );
10431 if !is_nothing_kw {
10432 return Err(self.err(format!(
10433 "expected NOTHING after DO in MERGE clause, got {:?}",
10434 self.peek()
10435 )));
10436 }
10437 self.advance();
10438 crate::ast::MergeAction::DoNothing
10439 }
10440 other => {
10441 return Err(self.err(format!(
10442 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10443 )));
10444 }
10445 };
10446 // PG's grammar simply has no INSERT production under BY SOURCE
10447 // (a target row already exists there) — same syntax error.
10448 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10449 && matches!(action, crate::ast::MergeAction::Insert { .. })
10450 {
10451 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10452 }
10453 clauses.push(crate::ast::MergeWhenClause {
10454 matched,
10455 condition,
10456 action,
10457 });
10458 }
10459 if clauses.is_empty() {
10460 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10461 }
10462 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10463 // unconditional (no `AND`) WHEN of the same match kind: it could
10464 // never fire. Check per match kind in clause order.
10465 let mut seen_unconditional_matched = false;
10466 let mut seen_unconditional_not_matched = false;
10467 let mut seen_unconditional_by_source = false;
10468 for c in &clauses {
10469 let seen = match c.matched {
10470 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10471 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10472 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10473 };
10474 if *seen {
10475 return Err(self.err(String::from(
10476 "unreachable WHEN clause specified after unconditional WHEN clause",
10477 )));
10478 }
10479 if c.condition.is_none() {
10480 *seen = true;
10481 }
10482 }
10483 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10484 let returning = self.parse_optional_returning()?;
10485 Ok(Statement::Merge(crate::ast::MergeStatement {
10486 // Attached by `parse_with_cte_then_select` when the MERGE
10487 // heads a WITH clause (round 149).
10488 ctes: Vec::new(),
10489 target,
10490 target_alias,
10491 source,
10492 source_alias,
10493 source_select,
10494 source_column_aliases,
10495 on,
10496 clauses,
10497 returning,
10498 }))
10499 }
10500
10501 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10502 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10503 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10504 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10505 fn parse_optional_returning(
10506 &mut self,
10507 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10508 let is_returning_kw = matches!(
10509 self.peek(),
10510 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10511 );
10512 if !is_returning_kw {
10513 return Ok(None);
10514 }
10515 self.advance();
10516 let mut items = Vec::new();
10517 loop {
10518 items.push(self.parse_select_item()?);
10519 if matches!(self.peek(), Token::Comma) {
10520 self.advance();
10521 continue;
10522 }
10523 break;
10524 }
10525 Ok(Some(items))
10526 }
10527
10528 /// v6.0.4 — parse the tail of an ALTER statement after the
10529 /// leading `ALTER` keyword has been consumed. Only one form is
10530 /// supported in v6.0.4:
10531 ///
10532 /// ```text
10533 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10534 /// ```
10535 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10536 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10537 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10538 // exclusion) is accepted by stripping the `ONLY` keyword
10539 // before the table parse.
10540 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10541 // and the long PG-dump tail are accepted as no-ops.
10542 match self.advance() {
10543 Token::Index => {}
10544 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10545 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10546 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10547 Token::Table => {
10548 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10549 self.advance();
10550 }
10551 return self.parse_alter_table_after_keyword();
10552 }
10553 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10554 return self.parse_alter_policy_after_keyword();
10555 }
10556 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10557 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10558 self.advance();
10559 }
10560 return self.parse_alter_table_after_keyword();
10561 }
10562 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10563 // of the silent-noop tail.
10564 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10565 return self.parse_alter_sequence_after_keyword();
10566 }
10567 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10568 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10569 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10570 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10571 // NB: the match arm consumed `TYPE` via self.advance(); the
10572 // cursor is now at the type name — do NOT advance again.
10573 let type_name = self.expect_ident_like()?;
10574 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10575 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10576 if is_add_value {
10577 self.advance(); // ADD
10578 self.advance(); // VALUE
10579 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10580 // IF/EXISTS as identifiers.
10581 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10582 {
10583 let n1 = self.tokens.get(self.pos + 1);
10584 let n2 = self.tokens.get(self.pos + 2);
10585 if matches!(n1, Some(Token::Not))
10586 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10587 {
10588 self.advance();
10589 self.advance();
10590 self.advance();
10591 true
10592 } else {
10593 false
10594 }
10595 } else {
10596 false
10597 };
10598 let label = self.expect_string_literal()?;
10599 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10600 {
10601 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10602 self.advance();
10603 let anchor = self.expect_string_literal()?;
10604 Some((is_before, anchor))
10605 } else {
10606 None
10607 };
10608 return Ok(Statement::AlterTypeAddValue {
10609 type_name,
10610 label,
10611 if_not_exists,
10612 position,
10613 });
10614 }
10615 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10616 // Used to fall into the no-op tail below: accepted, silently
10617 // ignored. `RENAME TO <newtype>` keeps falling through.
10618 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10619 && matches!(
10620 self.tokens.get(self.pos + 1),
10621 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10622 )
10623 {
10624 self.advance(); // RENAME
10625 self.advance(); // VALUE
10626 let old = self.expect_string_literal()?;
10627 if matches!(self.peek(), Token::To) {
10628 self.advance();
10629 } else {
10630 self.expect_keyword_ident("to")?;
10631 }
10632 let new = self.expect_string_literal()?;
10633 return Ok(Statement::AlterTypeRenameValue {
10634 type_name,
10635 old,
10636 new,
10637 });
10638 }
10639 // Other ALTER TYPE forms — the ACTION stays a no-op
10640 // (pg_dump tail), but v7.39 (round 708) the NAME is
10641 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10642 // success for a type that does not exist.
10643 self.consume_until_statement_boundary();
10644 return Ok(Statement::ValidateOnly {
10645 kind: crate::ast::ValidateOnlyKind::TypeName,
10646 names: alloc::vec![type_name],
10647 });
10648 }
10649 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10650 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10651 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10652 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10653 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10654 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10655 // pg_dump no-op list below: every form used to report success
10656 // and change nothing, which is worse than refusing outright
10657 // (a migration dropping a constraint kept rejecting data).
10658 // NOTE: the enclosing `match self.advance()` already consumed
10659 // the DOMAIN keyword, so the name is next.
10660 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10661 return self.parse_alter_domain_after_keyword();
10662 }
10663 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10664 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10665 // used to fall into the pg_dump no-op tail below, so a DBA
10666 // setting a per-role default was told it worked and nothing
10667 // happened. Intercepted here, BEFORE that tail.
10668 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10669 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10670 // interception below exists: swallowed with the no-op tail, an
10671 // unknown parameter name was ACCEPTED where PG18 answers
10672 // `unrecognized configuration parameter`. SPG applies nothing
10673 // either way — there is no postgresql.auto.conf — but it now
10674 // says so about a name it does not know.
10675 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10676 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10677 // already consumed here. An extra advance eats the SET and
10678 // the parameter name is never seen — which is exactly the
10679 // bug a panic in this branch disproved: the branch WAS on
10680 // the path, the reading of it was wrong.
10681 let mut parameter = None;
10682 // SET <name> … | RESET <name> | RESET ALL
10683 if matches!(self.peek(), Token::Ident(k)
10684 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10685 {
10686 self.advance();
10687 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10688 && !n.eq_ignore_ascii_case("all")
10689 {
10690 self.advance();
10691 // A dotted GUC (`plpgsql.check_asserts`) is two
10692 // tokens; keep the whole name.
10693 let mut full = n;
10694 while matches!(self.peek(), Token::Dot) {
10695 self.advance();
10696 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10697 full.push('.');
10698 full.push_str(&t);
10699 }
10700 }
10701 parameter = Some(full);
10702 }
10703 }
10704 self.consume_until_statement_boundary();
10705 return Ok(Statement::AlterSystem { parameter });
10706 }
10707 Token::Ident(s) | Token::QuotedIdent(s)
10708 if matches!(
10709 s.to_ascii_lowercase().as_str(),
10710 "role" | "user" | "database"
10711 ) && self.peeks_db_role_setting() =>
10712 {
10713 let is_database = s.eq_ignore_ascii_case("database");
10714 return self.parse_db_role_setting(is_database);
10715 }
10716 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10717 // (the non-SET forms; SET/RESET took the branch above). The
10718 // attributes still no-op — recorded, and the ignored PASSWORD
10719 // is ledgered as its own follow-up — but the ROLE is validated:
10720 // any name was accepted for a role that does not exist.
10721 Token::Ident(s) | Token::QuotedIdent(s)
10722 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10723 {
10724 // NB: the enclosing `match self.advance()` already consumed
10725 // ROLE/USER — the round-695 trap, hit again in this round's
10726 // first draft (the name was eaten and WITH parsed as the
10727 // role). The cursor is at the name.
10728 let name = self.expect_ident_or_string()?;
10729 // v7.39 (round 750) — scan the attribute tail for
10730 // PASSWORD. Everything else stays a recorded no-op, but
10731 // a dropped credential rotation is a SECURITY bug:
10732 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10733 // changed nothing, so the old password kept working.
10734 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10735 // NULL` clears the credential.
10736 let mut password: Option<Option<String>> = None;
10737 loop {
10738 match self.peek() {
10739 Token::Semicolon | Token::Eof => break,
10740 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10741 self.advance();
10742 match self.advance() {
10743 Token::String(p) => password = Some(Some(p)),
10744 Token::Null => password = Some(None),
10745 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10746 password = Some(None);
10747 }
10748 other => {
10749 return Err(self.err(alloc::format!(
10750 "expected password string or NULL after PASSWORD, got {other:?}"
10751 )));
10752 }
10753 }
10754 }
10755 _ => {
10756 self.advance();
10757 }
10758 }
10759 }
10760 if name.eq_ignore_ascii_case("all") {
10761 // `ALTER ROLE ALL …` names every role; nothing to check.
10762 return Ok(Statement::Empty);
10763 }
10764 if let Some(pw) = password {
10765 return Ok(Statement::AlterRolePassword { name, password: pw });
10766 }
10767 return Ok(Statement::ValidateOnly {
10768 kind: crate::ast::ValidateOnlyKind::RoleName,
10769 names: alloc::vec![name],
10770 });
10771 }
10772 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10773 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10774 // list far enough to validate the NAME; the actions still no-op.
10775 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10776 // models none of them and their dumps are rare.)
10777 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10778 let name = self.expect_ident_or_string()?;
10779 self.consume_until_statement_boundary();
10780 return Ok(Statement::ValidateOnly {
10781 kind: crate::ast::ValidateOnlyKind::CollationName,
10782 names: alloc::vec![name],
10783 });
10784 }
10785 Token::Ident(s) | Token::QuotedIdent(s)
10786 if s.eq_ignore_ascii_case("text")
10787 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10788 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10789 {
10790 self.advance(); // SEARCH
10791 self.advance(); // CONFIGURATION
10792 let name = self.expect_ident_like()?;
10793 self.consume_until_statement_boundary();
10794 return Ok(Statement::ValidateOnly {
10795 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10796 names: alloc::vec![name],
10797 });
10798 }
10799 Token::Ident(s) | Token::QuotedIdent(s)
10800 if s.eq_ignore_ascii_case("event")
10801 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10802 {
10803 self.advance(); // TRIGGER
10804 let name = self.expect_ident_like()?;
10805 self.consume_until_statement_boundary();
10806 return Ok(Statement::ValidateOnly {
10807 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10808 names: alloc::vec![name],
10809 });
10810 }
10811 Token::Ident(s) | Token::QuotedIdent(s)
10812 if s.eq_ignore_ascii_case("large")
10813 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10814 {
10815 self.advance(); // OBJECT
10816 let oid = match self.advance() {
10817 Token::Integer(n) => alloc::format!("{n}"),
10818 other => {
10819 return Err(
10820 self.err(alloc::format!("expected large object oid, got {other:?}"))
10821 );
10822 }
10823 };
10824 self.consume_until_statement_boundary();
10825 return Ok(Statement::ValidateOnly {
10826 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10827 names: alloc::vec![oid],
10828 });
10829 }
10830 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10831 // argument-list parse as DROP AGGREGATE (round 707); the
10832 // action no-ops, the existence check is real.
10833 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10834 // Same round-695 trap as above: AGGREGATE is already
10835 // consumed; the cursor is at the name.
10836 let name = self.expect_ident_like()?;
10837 let mut names = alloc::vec![name];
10838 if matches!(self.peek(), Token::LParen) {
10839 self.advance();
10840 loop {
10841 match self.peek().clone() {
10842 Token::RParen => {
10843 self.advance();
10844 break;
10845 }
10846 Token::Star => {
10847 self.advance();
10848 names.push(String::from("*"));
10849 }
10850 Token::Comma => {
10851 self.advance();
10852 }
10853 _ => {
10854 let mut t = self.expect_ident_like()?;
10855 while let Token::Ident(nx) = self.peek() {
10856 let nx = nx.clone();
10857 self.advance();
10858 t.push(' ');
10859 t.push_str(&nx);
10860 }
10861 names.push(t);
10862 }
10863 }
10864 }
10865 }
10866 self.consume_until_statement_boundary();
10867 return Ok(Statement::ValidateOnly {
10868 kind: crate::ast::ValidateOnlyKind::AggregateName,
10869 names,
10870 });
10871 }
10872 Token::Ident(s) | Token::QuotedIdent(s)
10873 if matches!(
10874 s.to_ascii_lowercase().as_str(),
10875 "view"
10876 | "function"
10877 | "database"
10878 | "schema"
10879 | "owner"
10880 | "default"
10881 | "extension"
10882 | "materialized"
10883 | "publication"
10884 | "subscription"
10885 // v7.37.17 (17.6 siblings) — additional ALTER
10886 // targets pg_dump / pg_dumpall / operator DB
10887 // migration scripts commonly emit. SPG has
10888 // no matching machinery for any of these; the
10889 // parser accepts + Empty-returns so pg_dump
10890 // tail statements don't stall.
10891 | "tablespace"
10892 | "language"
10893 | "operator"
10894 | "conversion"
10895 | "statistics"
10896 | "server"
10897 | "foreign"
10898 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10899 // / TEMPLATE (CONFIGURATION intercepted above).
10900 | "text"
10901 ) =>
10902 {
10903 self.consume_until_statement_boundary();
10904 return Ok(Statement::Empty);
10905 }
10906 other => {
10907 return Err(self.err(format!(
10908 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10909 after ALTER, got {other:?}"
10910 )));
10911 }
10912 }
10913 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10914 // (mailrs migrate-042 ships these). The presence of an
10915 // IF EXISTS makes the subsequent name lookup tolerate
10916 // a missing index — engine returns CommandOk no-op.
10917 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10918 let next = self.tokens.get(self.pos + 1);
10919 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10920 self.advance();
10921 self.advance();
10922 true
10923 } else {
10924 false
10925 }
10926 } else {
10927 false
10928 };
10929 let name = self.expect_ident_like()?;
10930 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10931 // Detect BEFORE the REBUILD path so the existing REBUILD
10932 // arm stays untouched.
10933 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10934 self.advance();
10935 if matches!(self.peek(), Token::To) {
10936 self.advance();
10937 } else {
10938 self.expect_keyword_ident("to")?;
10939 }
10940 let new = self.expect_ident_like()?;
10941 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10942 name,
10943 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10944 }));
10945 }
10946 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10947 // A syntax error before; the index is validated, the params no-op.
10948 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10949 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10950 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10951 {
10952 self.consume_until_statement_boundary();
10953 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10954 name,
10955 target: crate::ast::AlterIndexTarget::StorageParams,
10956 }));
10957 }
10958 // REBUILD
10959 self.expect_keyword_ident("rebuild")?;
10960 // Optional: WITH (encoding = <enc>)
10961 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10962 self.advance();
10963 if !matches!(self.peek(), Token::LParen) {
10964 return Err(self.err(format!(
10965 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10966 self.peek()
10967 )));
10968 }
10969 self.advance();
10970 self.expect_keyword_ident("encoding")?;
10971 if !matches!(self.peek(), Token::Eq) {
10972 return Err(self.err(format!(
10973 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10974 self.peek()
10975 )));
10976 }
10977 self.advance();
10978 let enc_ident = match self.advance() {
10979 Token::Ident(s) | Token::QuotedIdent(s) => s,
10980 other => {
10981 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10982 }
10983 };
10984 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10985 "f32" => VecEncoding::F32,
10986 "sq8" => VecEncoding::Sq8,
10987 "half" => VecEncoding::F16,
10988 other => {
10989 return Err(self.err(format!(
10990 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10991 )));
10992 }
10993 };
10994 if !matches!(self.peek(), Token::RParen) {
10995 return Err(self.err(format!(
10996 "expected ')' after encoding value, got {:?}",
10997 self.peek()
10998 )));
10999 }
11000 self.advance();
11001 Some(enc)
11002 } else {
11003 None
11004 };
11005 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
11006 name,
11007 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
11008 }))
11009 }
11010
11011 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
11012 /// only `SET` form currently supported; future v6.7.x can add
11013 /// more SET subjects without changing the dispatch shape.
11014 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
11015 /// subactions. Single-subaction shape stays a 1-element vec.
11016 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
11017 let table_name = self.expect_ident_like()?;
11018 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
11019 loop {
11020 let subaction = self.parse_alter_table_subaction()?;
11021 // ADD COLUMN with inline REFERENCES emits both an
11022 // AddColumn and an AddForeignKey subaction; the
11023 // helper returns 1 or 2 items.
11024 targets.extend(subaction);
11025 if matches!(self.peek(), Token::Comma) {
11026 self.advance();
11027 continue;
11028 }
11029 break;
11030 }
11031 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
11032 name: table_name,
11033 targets,
11034 }))
11035 }
11036
11037 /// Parse one ALTER TABLE subaction. Returns a Vec because
11038 /// inline `REFERENCES` on `ADD COLUMN` produces both an
11039 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
11040 /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
11041 /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
11042 /// appends.
11043 fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
11044 match self.peek() {
11045 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
11046 self.advance();
11047 Some(crate::ast::ColumnPosition::First)
11048 }
11049 Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
11050 self.advance();
11051 let name = self.expect_ident_like().ok()?;
11052 Some(crate::ast::ColumnPosition::After(name))
11053 }
11054 _ => None,
11055 }
11056 }
11057
11058 fn parse_alter_table_subaction(
11059 &mut self,
11060 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11061 match self.peek() {
11062 // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
11063 // a statement a real migration emits and SPG answered 1064
11064 // for; measured against MySQL 9.7.2, one at a time, beside
11065 // the published image.
11066 Token::Ident(s)
11067 if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
11068 {
11069 let changing = s.eq_ignore_ascii_case("change");
11070 self.advance();
11071 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
11072 self.advance();
11073 }
11074 // `parse_column_def_with_fk` reads the NAME itself, so
11075 // `MODIFY` hands it the column and `CHANGE` eats the old
11076 // name first and lets it read the new one.
11077 let old_name = if changing {
11078 Some(self.expect_ident_like()?)
11079 } else {
11080 None
11081 };
11082 let (definition, _fk) = self.parse_column_def_with_fk()?;
11083 let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11084 let rename_to = if changing {
11085 Some(definition.name.clone())
11086 } else {
11087 None
11088 };
11089 let position = self.parse_column_position();
11090 Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11091 column,
11092 rename_to,
11093 definition,
11094 position,
11095 }])
11096 }
11097 Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11098 self.advance();
11099 if matches!(self.peek(), Token::Eq) {
11100 self.advance();
11101 }
11102 let n = self.expect_u64_literal()?;
11103 Ok(alloc::vec![
11104 crate::ast::AlterTableTarget::SetTableAutoIncrement(
11105 i64::try_from(n).unwrap_or(i64::MAX)
11106 )
11107 ])
11108 }
11109 Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11110 self.advance();
11111 if matches!(self.peek(), Token::Eq) {
11112 self.advance();
11113 }
11114 // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11115 // clause has kept it since v7.39.3. The lexer folds a
11116 // bare identifier, and MySQL names the engine back
11117 // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11118 // answers `Unknown storage engine 'NoSuchEng'` there and
11119 // answered `'nosucheng'` here — the one thing that
11120 // message is for is telling the operator which word in
11121 // their migration was wrong.
11122 let at = self.pos;
11123 let name = self.expect_ident_like()?;
11124 let written = self
11125 .source_span(at, at)
11126 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11127 .filter(|raw| raw.eq_ignore_ascii_case(&name))
11128 .map(alloc::string::String::from);
11129 Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11130 written.unwrap_or(name)
11131 )])
11132 }
11133 Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11134 self.advance();
11135 // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11136 if matches!(self.peek(), Token::To) {
11137 self.advance();
11138 }
11139 let kw = self.expect_ident_like()?;
11140 if !kw.eq_ignore_ascii_case("character") {
11141 return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11142 }
11143 let set_kw = self.expect_ident_like()?;
11144 if !set_kw.eq_ignore_ascii_case("set") {
11145 return Err(self.err("expected SET after CHARACTER".into()));
11146 }
11147 let charset = self.expect_ident_like()?;
11148 let collate =
11149 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11150 self.advance();
11151 Some(self.expect_ident_like()?)
11152 } else {
11153 None
11154 };
11155 Ok(alloc::vec![
11156 crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11157 ])
11158 }
11159 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11160 self.advance();
11161 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11162 // storage parameters: paren-prefixed; consume.
11163 if matches!(self.peek(), Token::LParen) {
11164 self.consume_until_statement_boundary();
11165 return Ok(Vec::new());
11166 }
11167 let setting = self.expect_ident_like()?;
11168 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11169 if !matches!(self.peek(), Token::Eq) {
11170 return Err(self.err(alloc::format!(
11171 "expected '=' after hot_tier_bytes, got {:?}",
11172 self.peek()
11173 )));
11174 }
11175 self.advance();
11176 let n = self.expect_u64_literal()?;
11177 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11178 }
11179 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11180 // accept-and-no-op for ALTER TABLE SET <subject>
11181 // forms that pg_dump emits but SPG either treats
11182 // as N/A (single-tenant, single-owner, no shared
11183 // tablespaces) or accepts the dump-side declaration
11184 // without runtime effect:
11185 // SET SCHEMA <name> (18.11)
11186 // SET TABLESPACE <name> (18.8)
11187 // SET LOGGED / UNLOGGED (18.7 alt-form)
11188 // SET WITHOUT CLUSTER (18.13)
11189 // SET WITHOUT OIDS (PG legacy)
11190 // SET (option = value, …) (storage parameters)
11191 // SET REPLICA IDENTITY {…} (18.14)
11192 if setting.eq_ignore_ascii_case("schema")
11193 || setting.eq_ignore_ascii_case("tablespace")
11194 || setting.eq_ignore_ascii_case("logged")
11195 || setting.eq_ignore_ascii_case("unlogged")
11196 || setting.eq_ignore_ascii_case("without")
11197 {
11198 self.consume_until_statement_boundary();
11199 return Ok(Vec::new());
11200 }
11201 if setting.eq_ignore_ascii_case("replica") {
11202 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11203 self.consume_until_statement_boundary();
11204 return Ok(Vec::new());
11205 }
11206 // SET (option=value, …) — storage parameters.
11207 if matches!(self.peek(), Token::LParen) {
11208 self.consume_until_statement_boundary();
11209 return Ok(Vec::new());
11210 }
11211 Err(self.err(alloc::format!(
11212 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11213 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11214 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11215 )))
11216 }
11217 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11218 // not ignored: round 645 gave SPG the inheritance the
11219 // v7.37.18 no-op said it did not have.
11220 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11221 self.advance();
11222 let parent = self.expect_ident_like()?;
11223 self.consume_until_statement_boundary();
11224 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11225 parent,
11226 detach: false
11227 }])
11228 }
11229 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11230 // LEVEL SECURITY`, which has its own RLS arm below — without
11231 // the guard this swallowed NO FORCE as a no-op.
11232 Token::Ident(s)
11233 if s.eq_ignore_ascii_case("no")
11234 && !matches!(
11235 self.tokens.get(self.pos + 1),
11236 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11237 ) =>
11238 {
11239 self.advance();
11240 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11241 if k.eq_ignore_ascii_case("inherit"))
11242 {
11243 self.advance();
11244 let parent = self.expect_ident_like()?;
11245 self.consume_until_statement_boundary();
11246 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11247 parent,
11248 detach: true
11249 }]);
11250 }
11251 self.consume_until_statement_boundary();
11252 Ok(Vec::new())
11253 }
11254 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11255 // single-owner, so there is still nothing to record.
11256 //
11257 // v7.39 (round 652) — but the name now reaches the engine,
11258 // which refuses a role that does not exist as PG does. The
11259 // no-op was swallowing the whole statement, so a dump naming
11260 // a role this server never heard of restored clean and left
11261 // the table owned by whoever ran the restore.
11262 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11263 self.advance();
11264 if matches!(self.peek(), Token::To) {
11265 self.advance();
11266 }
11267 let role = self.expect_ident_like()?;
11268 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11269 role
11270 }])
11271 }
11272 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11273 // PG sets a hint; SPG doesn't have clustered storage, so the
11274 // hint itself stays a no-op.
11275 //
11276 // v7.39 (round 652) — the index name is checked now. PG
11277 // errors on one that does not exist, and swallowing that let
11278 // a typo'd CLUSTER ON pass silently.
11279 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11280 self.advance();
11281 // `ON` is a reserved token, not an ident.
11282 if !matches!(self.peek(), Token::On) {
11283 return Err(self.err(alloc::format!(
11284 "expected ON after CLUSTER, got {:?}",
11285 self.peek()
11286 )));
11287 }
11288 self.advance();
11289 let index = self.expect_ident_like()?;
11290 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11291 index: Some(index)
11292 }])
11293 }
11294 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11295 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11296 // what a logical decoder puts in the old-tuple image; SPG's
11297 // replication is SQL-text, so there is nothing to record.
11298 // Accept-and-no-op (it used to be a parse error).
11299 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11300 self.advance();
11301 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11302 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11303 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11304 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11305 {
11306 self.advance(); // IDENTITY
11307 self.advance(); // USING
11308 if matches!(self.peek(), Token::Index)
11309 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11310 {
11311 self.advance();
11312 }
11313 let index = self.expect_ident_like()?;
11314 self.consume_until_statement_boundary();
11315 return Ok(alloc::vec![
11316 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11317 ]);
11318 }
11319 self.consume_until_statement_boundary();
11320 Ok(Vec::new())
11321 }
11322 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11323 //
11324 // v7.39 (round 652) — it used to consume the statement and
11325 // return nothing, on the stated theory that SPG validated at
11326 // ADD CONSTRAINT time so there was never anything left to
11327 // validate. Measured against PG18, ADD CONSTRAINT did not
11328 // scan the existing rows at all — the comment described a
11329 // property SPG did not have, which is why nobody looked. Both
11330 // halves are real now: ADD scans unless told NOT VALID, and
11331 // this scans what NOT VALID skipped.
11332 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11333 self.advance();
11334 self.expect_keyword_ident("constraint")?;
11335 let name = self.expect_ident_like()?;
11336 Ok(alloc::vec![
11337 crate::ast::AlterTableTarget::ValidateConstraint { name }
11338 ])
11339 }
11340 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11341 // SET (option = value, …). PG uses it to clear per-table
11342 // storage params like fillfactor or autovacuum_*. SPG
11343 // engine-manages those parameters; accept-and-no-op.
11344 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11345 self.advance();
11346 self.consume_until_statement_boundary();
11347 Ok(Vec::new())
11348 }
11349 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11350 // type-of binding (PG 9.0+). SPG composite types
11351 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11352 // TABLE OF is rare and inverse of CREATE TABLE OF.
11353 // Accept-and-no-op until a customer dump round-trips it.
11354 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11355 self.advance();
11356 // v7.39 (round 710) — the type name is validated now.
11357 let type_name = self.expect_ident_like()?;
11358 self.consume_until_statement_boundary();
11359 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11360 type_name
11361 }])
11362 }
11363 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11364 // (reserved keyword) rather than Token::Ident("not"),
11365 // so it needs its own arm. Accept-and-no-op same as OF.
11366 Token::Not => {
11367 self.advance();
11368 self.consume_until_statement_boundary();
11369 Ok(Vec::new())
11370 }
11371 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11372 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11373 self.advance();
11374 self.expect_row_level_security()?;
11375 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11376 enabled: None,
11377 force: Some(true),
11378 }])
11379 }
11380 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11381 Token::Ident(s)
11382 if s.eq_ignore_ascii_case("no")
11383 && matches!(
11384 self.tokens.get(self.pos + 1),
11385 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11386 ) =>
11387 {
11388 self.advance(); // NO
11389 self.advance(); // FORCE
11390 self.expect_row_level_security()?;
11391 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11392 enabled: None,
11393 force: Some(false),
11394 }])
11395 }
11396 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11397 // (sets relrowsecurity). The guard requires the next token to be
11398 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11399 Token::Ident(s)
11400 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11401 && matches!(
11402 self.tokens.get(self.pos + 1),
11403 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11404 ) =>
11405 {
11406 let enabled = s.eq_ignore_ascii_case("enable");
11407 self.advance(); // ENABLE/DISABLE
11408 self.expect_row_level_security()?;
11409 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11410 enabled: Some(enabled),
11411 force: None,
11412 }])
11413 }
11414 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11415 self.advance();
11416 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11417 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11418 // emits. The same grammar CREATE TABLE already accepts
11419 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11420 // through the SAME parser — an ALTER-only copy would be a
11421 // second place for the two to drift.
11422 if self.peek_mysql_inline_key_start() {
11423 return Ok(match self.parse_mysql_inline_key()? {
11424 Some(c) => {
11425 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11426 }
11427 // FULLTEXT / SPATIAL parse and are accepted as a
11428 // no-op here exactly as they are inline.
11429 None => Vec::new(),
11430 });
11431 }
11432 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11433 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11434 // PRIMARY KEY this way; mysqldump emits both.
11435 // Peek-only dispatch (no advance) — `advance()`
11436 // destructively replaces consumed tokens with Eof,
11437 // so saved-pos restore would land on Eofs.
11438 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11439 {
11440 // The next-but-one ident is the constraint
11441 // name; the one after THAT is the kind.
11442 let kind_pos = self.pos + 2;
11443 let kind = self.tokens.get(kind_pos).cloned();
11444 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11445 {
11446 let fk = self.parse_table_level_fk()?;
11447 return Ok(alloc::vec![
11448 crate::ast::AlterTableTarget::AddForeignKey(fk)
11449 ]);
11450 }
11451 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11452 {
11453 self.advance(); // CONSTRAINT
11454 // v7.39 (read01 round 48) — keep the name; the engine
11455 // stores it now instead of dropping it on the floor.
11456 let con_name = self.expect_ident_like()?;
11457 self.advance(); // PRIMARY
11458 self.expect_keyword_ident("key")?;
11459 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11460 // v7.39 (round 711) — the ALTER form carries the
11461 // timing too (pg_dump writes it here).
11462 let (deferrable, initially_deferred) =
11463 self.consume_deferrable_clauses_timed()?;
11464 return Ok(alloc::vec![
11465 crate::ast::AlterTableTarget::AddTableConstraint(
11466 crate::ast::TableConstraint::PrimaryKey {
11467 name: Some(con_name),
11468 columns: cols,
11469 deferrable,
11470 initially_deferred,
11471 }
11472 )
11473 ]);
11474 }
11475 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11476 {
11477 self.advance(); // CONSTRAINT
11478 // v7.39 (read01 round 48) — keep the name.
11479 let con_name = self.expect_ident_like()?;
11480 // v7.22 (mailrs round-13 gap 6) — delegate so
11481 // the optional `NULLS [NOT] DISTINCT` modifier
11482 // parses here too (pg_dump emits the ALTER
11483 // form; semantics enforced by the engine
11484 // since v7.13).
11485 let mut uc = self.parse_table_level_unique()?;
11486 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11487 *name = Some(con_name);
11488 }
11489 return Ok(alloc::vec![
11490 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11491 ]);
11492 }
11493 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11494 {
11495 self.advance(); // CONSTRAINT
11496 // v7.39 (read01 round 48) — keep the name.
11497 let con_name = self.expect_ident_like()?;
11498 self.advance(); // CHECK
11499 if !matches!(self.peek(), Token::LParen) {
11500 return Err(self.err(alloc::format!(
11501 "expected '(' after CHECK, got {:?}", self.peek()
11502 )));
11503 }
11504 self.advance();
11505 let expr = self.parse_expr(0)?;
11506 if matches!(self.peek(), Token::RParen) {
11507 self.advance();
11508 }
11509 let not_valid = self.parse_not_valid_suffix();
11510 return Ok(alloc::vec![
11511 crate::ast::AlterTableTarget::AddTableConstraint(
11512 crate::ast::TableConstraint::Check {
11513 name: Some(con_name),
11514 expr,
11515 not_valid,
11516 }
11517 )
11518 ]);
11519 }
11520 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11521 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11522 // exclusion constraints via this ALTER form.
11523 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11524 {
11525 self.advance(); // CONSTRAINT
11526 let con_name = self.expect_ident_like()?;
11527 let mut ex = self.parse_table_level_exclude()?;
11528 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11529 *name = Some(con_name);
11530 }
11531 return Ok(alloc::vec![
11532 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11533 ]);
11534 }
11535 // Unknown kind — fall through to FK path which
11536 // produces a descriptive parse error.
11537 }
11538 let is_fk = matches!(
11539 self.peek(),
11540 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11541 || s.eq_ignore_ascii_case("foreign")
11542 );
11543 if is_fk {
11544 let fk = self.parse_table_level_fk()?;
11545 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11546 }
11547 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11548 // (no CONSTRAINT prefix) — same dispatch.
11549 match self.peek().clone() {
11550 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11551 self.advance();
11552 self.expect_keyword_ident("key")?;
11553 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11554 let (deferrable, initially_deferred) =
11555 self.consume_deferrable_clauses_timed()?;
11556 return Ok(alloc::vec![
11557 crate::ast::AlterTableTarget::AddTableConstraint(
11558 crate::ast::TableConstraint::PrimaryKey {
11559 name: None,
11560 columns: cols,
11561 deferrable,
11562 initially_deferred,
11563 }
11564 )
11565 ]);
11566 }
11567 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11568 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11569 let uc = self.parse_table_level_unique()?;
11570 return Ok(alloc::vec![
11571 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11572 ]);
11573 }
11574 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11575 // prefix). The other three bare forms were here and
11576 // this one was not, so it fell through to the column
11577 // path and came back as "unexpected reserved keyword
11578 // 'check' at start of column definition".
11579 _ if self.peek_table_level_check_start() => {
11580 let chk = self.parse_table_level_check()?;
11581 let not_valid = self.parse_not_valid_suffix();
11582 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11583 unreachable!("parse_table_level_check returns Check")
11584 };
11585 return Ok(alloc::vec![
11586 crate::ast::AlterTableTarget::AddTableConstraint(
11587 crate::ast::TableConstraint::Check {
11588 name: None,
11589 expr,
11590 not_valid,
11591 }
11592 )
11593 ]);
11594 }
11595 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11596 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11597 let ex = self.parse_table_level_exclude()?;
11598 return Ok(alloc::vec![
11599 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11600 ]);
11601 }
11602 _ => {}
11603 }
11604 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11605 self.advance();
11606 }
11607 let mut if_not_exists = false;
11608 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11609 self.advance();
11610 if !matches!(self.peek(), Token::Not) {
11611 return Err(self.err(alloc::format!(
11612 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11613 self.peek()
11614 )));
11615 }
11616 self.advance();
11617 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11618 return Err(self.err(alloc::format!(
11619 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11620 self.peek()
11621 )));
11622 }
11623 self.advance();
11624 if_not_exists = true;
11625 }
11626 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11627 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11628 // returns ColumnDef + an optional inline FK.
11629 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11630 let col_name = column.name.clone();
11631 // v7.39.9 — MySQL says where the column goes.
11632 let position = self.parse_column_position();
11633 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11634 column,
11635 if_not_exists,
11636 position,
11637 }];
11638 if let Some(mut fk) = col_level_fk {
11639 if fk.columns.is_empty() {
11640 fk.columns.push(col_name);
11641 }
11642 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11643 }
11644 Ok(out)
11645 }
11646 Token::Drop => {
11647 self.advance();
11648 // v7.13.3 — dispatch on the next token. mailrs round-7
11649 // S8 closed DROP COLUMN; round-6 S7 closed
11650 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11651 // RESTRICT modifiers.
11652 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11653 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11654 let subject = match self.peek() {
11655 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11656 self.advance();
11657 "constraint"
11658 }
11659 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11660 self.advance();
11661 "column"
11662 }
11663 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11664 // `INDEX` lexes as the reserved Token::Index, so it is
11665 // unambiguous. `KEY` is a plain ident, and PG allows a
11666 // column literally named "key", so only read it as the
11667 // keyword when a name follows it.
11668 Token::Index => {
11669 self.advance();
11670 "index"
11671 }
11672 Token::Ident(s)
11673 if s.eq_ignore_ascii_case("key")
11674 && matches!(
11675 self.tokens.get(self.pos + 1),
11676 Some(Token::Ident(_) | Token::QuotedIdent(_))
11677 ) =>
11678 {
11679 self.advance();
11680 "index"
11681 }
11682 // PG-canonical bare `DROP <col>` without COLUMN
11683 // keyword is also valid; treat any other ident
11684 // as the column name.
11685 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11686 other => {
11687 return Err(self.err(alloc::format!(
11688 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11689 )));
11690 }
11691 };
11692 let mut if_exists = false;
11693 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11694 let n1 = self.tokens.get(self.pos + 1);
11695 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11696 self.advance();
11697 self.advance();
11698 if_exists = true;
11699 }
11700 }
11701 let name = self.expect_ident_like()?;
11702 let mut cascade = false;
11703 if matches!(
11704 self.peek(),
11705 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11706 || s.eq_ignore_ascii_case("restrict")
11707 ) {
11708 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11709 {
11710 cascade = true;
11711 }
11712 self.advance();
11713 }
11714 if subject == "index" {
11715 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11716 name,
11717 if_exists,
11718 }])
11719 } else if subject == "constraint" {
11720 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11721 name,
11722 if_exists,
11723 }])
11724 } else {
11725 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11726 column: name,
11727 if_exists,
11728 cascade,
11729 }])
11730 }
11731 }
11732 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11733 self.advance();
11734 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11735 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11736 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11737 // immediately; accept-and-no-op.
11738 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11739 self.advance();
11740 self.consume_until_statement_boundary();
11741 return Ok(Vec::new());
11742 }
11743 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11744 self.advance();
11745 }
11746 let col_name = self.expect_ident_like()?;
11747 match self.peek() {
11748 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11749 self.advance();
11750 }
11751 // v7.14.0 — pg_dump emits BIGSERIAL via
11752 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11753 // nextval('seq')` (the sequence is created
11754 // separately). SPG's BIGSERIAL already uses
11755 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11756 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11757 // engine no-ops by consuming the tail.
11758 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11759 // v7.22 (round-13 T2) — `SET DEFAULT
11760 // nextval('…')` is how pg_dump spells a
11761 // SERIAL column (plain integer in CREATE
11762 // TABLE + this ALTER). It used to be
11763 // swallowed as a no-op, which silently
11764 // STRIPPED auto-increment from imported
11765 // schemas — the first post-import INSERT
11766 // without an explicit id then violated NOT
11767 // NULL. Lower it to the auto-increment
11768 // marker instead.
11769 let is_default_nextval =
11770 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11771 && matches!(
11772 self.tokens.get(self.pos + 2),
11773 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11774 );
11775 if is_default_nextval {
11776 let seq_name = self.scan_sequence_name_until_boundary();
11777 return Ok(alloc::vec![
11778 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11779 column: col_name,
11780 seq_name,
11781 }
11782 ]);
11783 }
11784 // v7.37.18 (18.1 + 18.2) — proper lowering.
11785 self.advance(); // consume "set"
11786 match self.peek().clone() {
11787 Token::Default => {
11788 self.advance();
11789 let default_expr = self.parse_expr(0)?;
11790 return Ok(alloc::vec![
11791 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11792 column: col_name,
11793 default_expr,
11794 }
11795 ]);
11796 }
11797 Token::Not => {
11798 self.advance();
11799 if !matches!(self.peek(), Token::Null) {
11800 return Err(self.err(alloc::format!(
11801 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11802 self.peek()
11803 )));
11804 }
11805 self.advance();
11806 return Ok(alloc::vec![
11807 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11808 column: col_name,
11809 }
11810 ]);
11811 }
11812 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11813 // stored generated column's expression and
11814 // recompute existing rows.
11815 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11816 self.advance(); // EXPRESSION
11817 if matches!(self.peek(), Token::As) {
11818 self.advance();
11819 }
11820 let expr = self.parse_expr(0)?;
11821 return Ok(alloc::vec![
11822 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11823 column: col_name,
11824 expr,
11825 }
11826 ]);
11827 }
11828 other => {
11829 // Other SET subjects (STATISTICS,
11830 // STORAGE, COMPRESSION, …) stay no-ops —
11831 // storage hints with no SPG semantics.
11832 let _ = other;
11833 self.consume_until_statement_boundary();
11834 return Ok(Vec::new());
11835 }
11836 }
11837 }
11838 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11839 self.advance(); // consume "drop"
11840 return self.parse_alter_column_drop_tail(col_name);
11841 }
11842 Token::Drop => {
11843 self.advance(); // consume Drop token
11844 return self.parse_alter_column_drop_tail(col_name);
11845 }
11846 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11847 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11848 // GENERATED { ALWAYS | BY DEFAULT } AS
11849 // IDENTITY ( … )`: pg_dump's spelling for
11850 // identity columns. Same auto-increment
11851 // lowering as the nextval default; the
11852 // sequence options inside the parens are
11853 // no-ops under SPG's max+1 semantics.
11854 let is_generated = matches!(
11855 self.tokens.get(self.pos + 1),
11856 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11857 );
11858 if !is_generated {
11859 return Err(self.err(alloc::format!(
11860 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11861 self.tokens.get(self.pos + 1)
11862 )));
11863 }
11864 let seq_name = self.scan_sequence_name_until_boundary();
11865 return Ok(alloc::vec![
11866 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11867 column: col_name,
11868 seq_name,
11869 }
11870 ]);
11871 }
11872 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11873 // column: floor the next allocated value at n (bare
11874 // RESTART = restart from the start value, 1).
11875 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11876 self.advance();
11877 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11878 {
11879 self.advance();
11880 let neg = if matches!(self.peek(), Token::Minus) {
11881 self.advance();
11882 true
11883 } else {
11884 false
11885 };
11886 match self.advance() {
11887 Token::Integer(v) => Some(if neg { -v } else { v }),
11888 other => {
11889 return Err(self.err(alloc::format!(
11890 "expected integer after RESTART WITH, got {other:?}"
11891 )));
11892 }
11893 }
11894 } else {
11895 None
11896 };
11897 return Ok(alloc::vec![
11898 crate::ast::AlterTableTarget::AlterColumnRestart {
11899 column: col_name,
11900 with,
11901 }
11902 ]);
11903 }
11904 other => {
11905 return Err(self.err(alloc::format!(
11906 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11907 )));
11908 }
11909 }
11910 // v7.39 (round 713) — the type parser has consumed a
11911 // trailing `COLLATE <name>` since Phase 2.5, and
11912 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11913 // TYPE text COLLATE "C"` parsed clean and changed
11914 // nothing. Keep the clause; the engine re-collates.
11915 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11916 self.parse_type_with_implied_flags()?;
11917 let collation = if coll_explicit {
11918 coll_name.map(|n| (coll, n))
11919 } else {
11920 None
11921 };
11922 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11923 {
11924 self.advance();
11925 Some(self.parse_expr(0)?)
11926 } else {
11927 None
11928 };
11929 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11930 column: col_name,
11931 new_type,
11932 using,
11933 collation,
11934 }])
11935 }
11936 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11937 // PG also supports `RENAME TO new_table` for table-name
11938 // rename; that surface is deferred (pg_dump never emits
11939 // it). If the first post-RENAME ident is `TO`, the user
11940 // is asking for table rename — error with a clear
11941 // message rather than misparsing `TO` as a column name.
11942 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11943 self.advance();
11944 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11945 // table-name rename (mailrs round-10 A.5 — used
11946 // by migrate-042's `RENAME TO email_contacts`).
11947 // `TO` lexes as Token::To.
11948 if matches!(self.peek(), Token::To)
11949 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11950 {
11951 self.advance();
11952 let new = self.expect_ident_like()?;
11953 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11954 new,
11955 }]);
11956 }
11957 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11958 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11959 self.advance();
11960 let old = self.expect_ident_like()?;
11961 if matches!(self.peek(), Token::To) {
11962 self.advance();
11963 } else {
11964 self.expect_keyword_ident("to")?;
11965 }
11966 let new = self.expect_ident_like()?;
11967 return Ok(alloc::vec![
11968 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11969 ]);
11970 }
11971 // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11972 // PostgreSQL renames an index with its own top-level
11973 // `ALTER INDEX`, so this spelling had nowhere to go and
11974 // answered 1064; MySQL 9.7.2 parses it and answers 1176
11975 // when the key is not there.
11976 if matches!(self.peek(), Token::Index)
11977 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11978 {
11979 self.advance();
11980 let old = self.expect_ident_like()?;
11981 if matches!(self.peek(), Token::To) {
11982 self.advance();
11983 } else {
11984 self.expect_keyword_ident("to")?;
11985 }
11986 let new = self.expect_ident_like()?;
11987 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11988 old,
11989 new,
11990 }]);
11991 }
11992 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11993 self.advance();
11994 }
11995 let old = self.expect_ident_like()?;
11996 // `TO` is a reserved keyword token; accept both
11997 // Token::To and Token::Ident("to") for consistency.
11998 if matches!(self.peek(), Token::To) {
11999 self.advance();
12000 } else {
12001 self.expect_keyword_ident("to")?;
12002 }
12003 let new = self.expect_ident_like()?;
12004 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
12005 old,
12006 new,
12007 }])
12008 }
12009 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
12010 // { ALL | <name> }`. pg_dump --disable-triggers wraps
12011 // every data block with these. Real disable semantics —
12012 // not no-op — because reload correctness assumes the
12013 // triggers don't fire (rows already carry their
12014 // computed values from prod).
12015 Token::Ident(s)
12016 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
12017 {
12018 let enabled = s.eq_ignore_ascii_case("enable");
12019 self.advance();
12020 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
12021 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
12022 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
12023 // pg_dump output) — anything else falls through to
12024 // the catch-all error below.
12025 // v7.22 (round-13 T3) — mysqldump wraps every data
12026 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
12027 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
12028 // maintains indexes incrementally — engine no-op.
12029 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
12030 self.advance();
12031 return Ok(Vec::new());
12032 }
12033 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
12034 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
12035 // to gate triggers on session_replication_role; SPG
12036 // has no replica role, so the prefix is consumed and
12037 // treated identically to the plain ENABLE/DISABLE
12038 // TRIGGER form.
12039 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
12040 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
12041 {
12042 self.advance();
12043 }
12044 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
12045 return Err(self.err(alloc::format!(
12046 "expected TRIGGER after {}, got {:?}",
12047 if enabled { "ENABLE" } else { "DISABLE" },
12048 self.peek()
12049 )));
12050 }
12051 self.advance();
12052 // `ALL` lexes as Token::All (reserved); also
12053 // accept Token::Ident("all") for symmetry.
12054 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
12055 // TRIGGER selectors. USER (= all user triggers) is
12056 // semantically ALL here; REPLICA / ALWAYS gate on
12057 // session_replication_role which SPG doesn't track.
12058 // All map to TriggerSelector::All.
12059 let which = if matches!(self.peek(), Token::All)
12060 || matches!(self.peek(), Token::Ident(s)
12061 if s.eq_ignore_ascii_case("all")
12062 || s.eq_ignore_ascii_case("user")
12063 || s.eq_ignore_ascii_case("replica")
12064 || s.eq_ignore_ascii_case("always"))
12065 {
12066 self.advance();
12067 crate::ast::TriggerSelector::All
12068 } else {
12069 let name = self.expect_ident_like()?;
12070 crate::ast::TriggerSelector::Named(name)
12071 };
12072 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
12073 which,
12074 enabled,
12075 }])
12076 }
12077 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
12078 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
12079 self.advance();
12080 if !matches!(self.peek(), Token::Partition)
12081 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12082 if s.eq_ignore_ascii_case("partition"))
12083 {
12084 return Err(self.err(alloc::format!(
12085 "expected PARTITION after ATTACH, got {:?}",
12086 self.peek()
12087 )));
12088 }
12089 self.advance();
12090 let child = self.expect_ident_like()?;
12091 let bounds = self.parse_partition_bounds_tail()?;
12092 Ok(alloc::vec![
12093 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12094 ])
12095 }
12096 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12097 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12098 self.advance();
12099 if !matches!(self.peek(), Token::Partition)
12100 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12101 if s.eq_ignore_ascii_case("partition"))
12102 {
12103 return Err(self.err(alloc::format!(
12104 "expected PARTITION after DETACH, got {:?}",
12105 self.peek()
12106 )));
12107 }
12108 self.advance();
12109 let child = self.expect_ident_like()?;
12110 let mut concurrently = false;
12111 let mut finalize = false;
12112 loop {
12113 match self.peek().clone() {
12114 Token::Ident(s) | Token::QuotedIdent(s)
12115 if s.eq_ignore_ascii_case("concurrently") =>
12116 {
12117 self.advance();
12118 concurrently = true;
12119 }
12120 Token::Ident(s) | Token::QuotedIdent(s)
12121 if s.eq_ignore_ascii_case("finalize") =>
12122 {
12123 self.advance();
12124 finalize = true;
12125 }
12126 _ => break,
12127 }
12128 }
12129 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12130 child,
12131 concurrently,
12132 finalize,
12133 }])
12134 }
12135 other => Err(self.err(alloc::format!(
12136 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12137 ))),
12138 }
12139 }
12140
12141 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12142 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12143 /// TABLE … ATTACH PARTITION. Shares the same grammar as
12144 /// `parse_partition_of_tail`'s bounds branch.
12145 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12146 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12147 /// lowering each to the respective AlterTableTarget. Any
12148 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12149 /// no-op via consume_until_statement_boundary.
12150 fn parse_alter_column_drop_tail(
12151 &mut self,
12152 col_name: String,
12153 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12154 match self.peek().clone() {
12155 Token::Default => {
12156 self.advance();
12157 Ok(alloc::vec![
12158 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12159 ])
12160 }
12161 Token::Not => {
12162 self.advance();
12163 if !matches!(self.peek(), Token::Null) {
12164 return Err(self.err(alloc::format!(
12165 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12166 self.peek()
12167 )));
12168 }
12169 self.advance();
12170 Ok(alloc::vec![
12171 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12172 ])
12173 }
12174 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12175 // generated column into a plain column.
12176 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12177 self.advance();
12178 // v7.39 (round 187, U10) — IF EXISTS was consumed but
12179 // dropped, so the engine still errored on a plain
12180 // column; PG's semantics are NOTICE + skip.
12181 let mut if_exists = false;
12182 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12183 self.advance();
12184 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12185 self.advance();
12186 if_exists = true;
12187 }
12188 }
12189 Ok(alloc::vec![
12190 crate::ast::AlterTableTarget::AlterColumnDropExpression {
12191 column: col_name,
12192 if_exists,
12193 }
12194 ])
12195 }
12196 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12197 // identity column into a plain column.
12198 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12199 self.advance();
12200 let mut if_exists = false;
12201 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12202 self.advance();
12203 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12204 self.advance();
12205 if_exists = true;
12206 }
12207 }
12208 Ok(alloc::vec![
12209 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12210 column: col_name,
12211 if_exists,
12212 }
12213 ])
12214 }
12215 _ => {
12216 self.consume_until_statement_boundary();
12217 Ok(Vec::new())
12218 }
12219 }
12220 }
12221
12222 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12223 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12224 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12225 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12226 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12227 let mut opts = crate::ast::CopyOptions::default();
12228 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12229 return Ok(opts);
12230 }
12231 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12232 self.advance();
12233 }
12234 if matches!(self.peek(), Token::LParen) {
12235 self.advance();
12236 loop {
12237 self.parse_one_copy_option(&mut opts)?;
12238 match self.peek() {
12239 Token::Comma => {
12240 self.advance();
12241 }
12242 Token::RParen => {
12243 self.advance();
12244 break;
12245 }
12246 other => {
12247 return Err(self.err(alloc::format!(
12248 "expected ',' or ')' in COPY options, got {other:?}"
12249 )));
12250 }
12251 }
12252 }
12253 } else {
12254 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12255 self.parse_one_copy_option(&mut opts)?;
12256 }
12257 }
12258 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12259 return Err(self.err(alloc::format!(
12260 "unexpected token after COPY options: {:?}",
12261 self.peek()
12262 )));
12263 }
12264 Ok(opts)
12265 }
12266
12267 fn parse_one_copy_option(
12268 &mut self,
12269 opts: &mut crate::ast::CopyOptions,
12270 ) -> Result<(), ParseError> {
12271 use crate::ast::CopyFormat;
12272 // The option keyword. NULL lexes as its own token; the rest are
12273 // bare identifiers.
12274 let kw = match self.advance() {
12275 Token::Null => alloc::string::String::from("NULL"),
12276 Token::Ident(s) => s.to_uppercase(),
12277 other => {
12278 return Err(self.err(alloc::format!(
12279 "expected a COPY option keyword, got {other:?}"
12280 )));
12281 }
12282 };
12283 match kw.as_str() {
12284 "FORMAT" => {
12285 let fmt = self.expect_ident_like()?;
12286 match fmt.to_ascii_uppercase().as_str() {
12287 "CSV" => opts.format = CopyFormat::Csv,
12288 "TEXT" => opts.format = CopyFormat::Text,
12289 other => {
12290 return Err(self.err(alloc::format!(
12291 "COPY format \"{}\" not recognized",
12292 other.to_ascii_lowercase()
12293 )));
12294 }
12295 }
12296 }
12297 // Legacy bare format keywords.
12298 "CSV" => opts.format = CopyFormat::Csv,
12299 "TEXT" => opts.format = CopyFormat::Text,
12300 "HEADER" => {
12301 opts.header = match self.peek() {
12302 Token::True => {
12303 self.advance();
12304 true
12305 }
12306 Token::False => {
12307 self.advance();
12308 false
12309 }
12310 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12311 self.advance();
12312 true
12313 }
12314 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12315 self.advance();
12316 false
12317 }
12318 // Bare HEADER (no boolean) means HEADER true.
12319 _ => true,
12320 };
12321 }
12322 // r1066 (7.38 S5.1) — pgbench 14+ loads with
12323 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12324 // vacuum bookkeeping on a freshly created/truncated
12325 // table; SPG's per-statement visibility makes it a
12326 // faithful no-op, and rejecting it aborted `pgbench -i`
12327 // against the drop-in. Accept ON/OFF/bare, change nothing.
12328 "FREEZE" => match self.peek() {
12329 Token::True | Token::False => {
12330 self.advance();
12331 }
12332 Token::Ident(s)
12333 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12334 {
12335 self.advance();
12336 }
12337 _ => {}
12338 },
12339 "DELIMITER" | "QUOTE" | "ESCAPE" => {
12340 let s = match self.advance() {
12341 Token::String(s) => s,
12342 other => {
12343 return Err(self.err(alloc::format!(
12344 "COPY {kw} expects a single-character string, got {other:?}"
12345 )));
12346 }
12347 };
12348 // v7.39 (round 247) — PG's wording (0A000), keyword in
12349 // lowercase: "COPY delimiter must be a single one-byte
12350 // character".
12351 let one_byte_err = || {
12352 self.err(alloc::format!(
12353 "COPY {} must be a single one-byte character",
12354 kw.to_ascii_lowercase()
12355 ))
12356 };
12357 let mut chars = s.chars();
12358 let c = chars.next().ok_or_else(one_byte_err)?;
12359 if chars.next().is_some() || c.len_utf8() != 1 {
12360 return Err(one_byte_err());
12361 }
12362 match kw.as_str() {
12363 "DELIMITER" => opts.delimiter = Some(c),
12364 "QUOTE" => opts.quote = Some(c),
12365 _ => opts.escape = Some(c),
12366 }
12367 }
12368 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12369 "FORCE_QUOTE" => {
12370 if matches!(self.peek(), Token::Star) {
12371 self.advance();
12372 opts.force_quote = Some(Vec::new());
12373 } else {
12374 if !matches!(self.peek(), Token::LParen) {
12375 return Err(self.err(alloc::format!(
12376 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12377 self.peek()
12378 )));
12379 }
12380 self.advance();
12381 let mut cols = Vec::new();
12382 loop {
12383 cols.push(self.expect_ident_like()?);
12384 match self.peek() {
12385 Token::Comma => {
12386 self.advance();
12387 }
12388 Token::RParen => {
12389 self.advance();
12390 break;
12391 }
12392 other => {
12393 return Err(self.err(alloc::format!(
12394 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12395 )));
12396 }
12397 }
12398 }
12399 opts.force_quote = Some(cols);
12400 }
12401 }
12402 "NULL" => {
12403 opts.null_str = Some(match self.advance() {
12404 Token::String(s) => s,
12405 other => {
12406 return Err(self.err(alloc::format!(
12407 "COPY NULL expects a quoted string, got {other:?}"
12408 )));
12409 }
12410 });
12411 }
12412 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12413 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12414 // FORCE_NULL too.
12415 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12416 let cols = self.parse_copy_column_list(&kw)?;
12417 if kw == "FORCE_NOT_NULL" {
12418 opts.force_not_null = Some(cols);
12419 } else {
12420 opts.force_null = Some(cols);
12421 }
12422 }
12423 other => {
12424 // PG's wording, lowercased option name.
12425 return Err(self.err(alloc::format!(
12426 "option \"{}\" not recognized",
12427 other.to_ascii_lowercase()
12428 )));
12429 }
12430 }
12431 Ok(())
12432 }
12433
12434 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12435 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12436 /// is the `*` spelling.
12437 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12438 if matches!(self.peek(), Token::Star) {
12439 self.advance();
12440 return Ok(Vec::new());
12441 }
12442 if !matches!(self.peek(), Token::LParen) {
12443 return Err(self.err(alloc::format!(
12444 "expected '(' or '*' after {kw}, got {:?}",
12445 self.peek()
12446 )));
12447 }
12448 self.advance();
12449 let mut cols = Vec::new();
12450 loop {
12451 cols.push(self.expect_ident_like()?);
12452 match self.peek() {
12453 Token::Comma => {
12454 self.advance();
12455 }
12456 Token::RParen => {
12457 self.advance();
12458 break;
12459 }
12460 other => {
12461 return Err(self.err(alloc::format!(
12462 "expected ',' or ')' in {kw} list, got {other:?}"
12463 )));
12464 }
12465 }
12466 }
12467 Ok(cols)
12468 }
12469
12470 fn parse_partition_bounds_tail(
12471 &mut self,
12472 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12473 use crate::ast::PartitionOfBoundsAst;
12474 match self.peek() {
12475 Token::Default => {
12476 self.advance();
12477 Ok(PartitionOfBoundsAst::Default)
12478 }
12479 Token::For => {
12480 self.advance();
12481 if !matches!(self.peek(), Token::Values) {
12482 return Err(
12483 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12484 );
12485 }
12486 self.advance();
12487 let want_with = matches!(
12488 self.peek(),
12489 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12490 );
12491 if want_with {
12492 self.advance();
12493 if !matches!(self.peek(), Token::LParen) {
12494 return Err(self.err(format!(
12495 "expected '(' after FOR VALUES WITH, got {:?}",
12496 self.peek()
12497 )));
12498 }
12499 self.advance();
12500 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12501 loop {
12502 let key = self.expect_ident_like()?;
12503 let n = match self.peek().clone() {
12504 Token::Integer(v) if u32::try_from(v).is_ok() => {
12505 self.advance();
12506 v as u32
12507 }
12508 other => {
12509 return Err(self.err(format!(
12510 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12511 )));
12512 }
12513 };
12514 match key.to_ascii_uppercase().as_str() {
12515 "MODULUS" => modulus = Some(n),
12516 "REMAINDER" => remainder = Some(n),
12517 other => {
12518 return Err(self.err(format!(
12519 "FOR VALUES WITH: unknown key {other:?}; \
12520 expected MODULUS or REMAINDER"
12521 )));
12522 }
12523 }
12524 match self.peek() {
12525 Token::Comma => {
12526 self.advance();
12527 }
12528 Token::RParen => {
12529 self.advance();
12530 break;
12531 }
12532 other => {
12533 return Err(self.err(format!(
12534 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12535 )));
12536 }
12537 }
12538 }
12539 let modulus = modulus
12540 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12541 let remainder = remainder.ok_or_else(|| {
12542 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12543 })?;
12544 if modulus == 0 {
12545 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12546 }
12547 if remainder >= modulus {
12548 return Err(self.err(format!(
12549 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12550 )));
12551 }
12552 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12553 }
12554 match self.peek() {
12555 Token::From => {
12556 self.advance();
12557 let lower = Box::new(self.parse_partition_bound_expr()?);
12558 if !matches!(self.peek(), Token::To) {
12559 return Err(self.err(format!(
12560 "expected TO after FROM (...), got {:?}",
12561 self.peek()
12562 )));
12563 }
12564 self.advance();
12565 let upper = Box::new(self.parse_partition_bound_expr()?);
12566 Ok(PartitionOfBoundsAst::Range { lower, upper })
12567 }
12568 Token::In => {
12569 self.advance();
12570 if !matches!(self.peek(), Token::LParen) {
12571 return Err(self.err(format!(
12572 "expected '(' after FOR VALUES IN, got {:?}",
12573 self.peek()
12574 )));
12575 }
12576 self.advance();
12577 let mut values = Vec::new();
12578 loop {
12579 values.push(self.parse_expr(0)?);
12580 match self.peek() {
12581 Token::Comma => {
12582 self.advance();
12583 }
12584 Token::RParen => {
12585 self.advance();
12586 break;
12587 }
12588 other => {
12589 return Err(self.err(format!(
12590 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12591 )));
12592 }
12593 }
12594 }
12595 if values.is_empty() {
12596 return Err(
12597 self.err("FOR VALUES IN requires at least one literal".to_string())
12598 );
12599 }
12600 Ok(PartitionOfBoundsAst::List { values })
12601 }
12602 other => Err(self.err(format!(
12603 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12604 ))),
12605 }
12606 }
12607 other => Err(self.err(format!(
12608 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12609 ))),
12610 }
12611 }
12612
12613 /// v7.16.2 — peek for `information_schema.<tbl>` /
12614 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12615 /// three tokens + return a synthetic table name the engine's
12616 /// SELECT path recognises as a virtual view. Returns `None`
12617 /// when the head doesn't look like a meta-qualified name.
12618 /// Used by `parse_table_ref` to bypass the
12619 /// `expect_ident_like` schema-strip for these specific PG
12620 /// meta schemas (mailrs round-10 A.3).
12621 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12622 // Extract the schema name. Must be a plain ident token.
12623 let schema = match self.tokens.get(self.pos) {
12624 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12625 _ => return None,
12626 };
12627 // Dot.
12628 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12629 return None;
12630 }
12631 // The table-side ident may lex as a reserved keyword
12632 // (e.g. `Token::Tables`). Tolerate the common ones via a
12633 // helper that reads the trailing token's underlying name.
12634 let tbl = match self.tokens.get(self.pos + 2)? {
12635 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12636 Token::Tables => "tables".to_string(),
12637 // Other PG meta table names that may collide with
12638 // reserved keywords land here as needed.
12639 _ => return None,
12640 };
12641 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12642 // names so the synthetic name doesn't double-prefix
12643 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12644 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12645 ("__spg_info_", tbl.to_ascii_lowercase())
12646 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12647 // v7.39 (round 541) — only the catalogs SPG actually
12648 // synthesises are rewritten, which is what the BARE path
12649 // has always checked. Anything else keeps its own name and
12650 // takes the ordinary route: `pg_stat_activity` and friends
12651 // resolve through meta_view_result, and a name that is no
12652 // catalog at all gets PG's "relation does not exist"
12653 // instead of a message about a view SPG cannot materialise.
12654 let lowered = tbl.to_ascii_lowercase();
12655 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12656 self.advance(); // schema
12657 self.advance(); // dot
12658 self.advance(); // tbl
12659 return Some((lowered.clone(), lowered));
12660 }
12661 let bare = lowered
12662 .strip_prefix("pg_")
12663 .map(alloc::string::String::from)
12664 .unwrap_or(lowered);
12665 ("__spg_pg_", bare)
12666 } else if schema.eq_ignore_ascii_case("mysql") {
12667 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12668 // (`mysql.user`, `mysql.db`). Same synthetic-name
12669 // shape as pg_catalog.
12670 ("__spg_mysql_", tbl.to_ascii_lowercase())
12671 } else {
12672 return None;
12673 };
12674 self.advance(); // schema
12675 self.advance(); // dot
12676 self.advance(); // tbl
12677 Some((
12678 alloc::format!("{prefix}{normalised}"),
12679 tbl.to_ascii_lowercase(),
12680 ))
12681 }
12682
12683 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12684 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12685 /// implicit front of every search_path, so a bare reference to a
12686 /// known catalog table always means the catalog table. Only the
12687 /// names the engine actually synthesises are recognised — any
12688 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12689 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12690 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12691 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12692 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12693 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12694 // through the meta_view_result path instead, and already resolve
12695 // bare — they must NOT be listed here or the __spg_ rewrite would
12696 // mis-target them.)
12697 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12698 let name = match self.tokens.get(self.pos) {
12699 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12700 _ => return None,
12701 };
12702 // A following dot means this ident is a schema qualifier,
12703 // not a table name — let the qualified path handle it.
12704 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12705 return None;
12706 }
12707 if !PG_META_TABLES.contains(&name.as_str()) {
12708 return None;
12709 }
12710 self.advance();
12711 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12712 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12713 }
12714
12715 /// Consume a bare ident if its lowercase matches `kw`, else err.
12716 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12717 /// Peeks only; the caller advances.
12718 fn peek_keyword_ident(&self, kw: &str) -> bool {
12719 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12720 }
12721
12722 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12723 match self.advance() {
12724 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12725 other => Err(ParseError {
12726 message: format!("expected {kw:?}, got {other:?}"),
12727 token_pos: self.consumed_pos(),
12728 }),
12729 }
12730 }
12731
12732 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12733 /// literal (`'foo'`) — same shape used by CREATE USER for the
12734 /// username slot.
12735 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12736 match self.advance() {
12737 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12738 other => Err(ParseError {
12739 message: format!("expected identifier or string, got {other:?}"),
12740 token_pos: self.consumed_pos(),
12741 }),
12742 }
12743 }
12744
12745 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12746 match self.advance() {
12747 Token::String(s) => Ok(s),
12748 other => Err(ParseError {
12749 message: format!("expected quoted string, got {other:?}"),
12750 token_pos: self.consumed_pos(),
12751 }),
12752 }
12753 }
12754
12755 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12756 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12757 // subqueries recurse through here without passing
12758 // parse_expr; share the same nesting budget.
12759 self.enter_nested()?;
12760 let r = self.parse_select_stmt_inner();
12761 self.nest_depth -= 1;
12762 r
12763 }
12764
12765 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12766 // Caller dispatches on Token::Select; the inner helper handles
12767 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12768 // get a fresh bare-select parse and may not have their own ORDER
12769 // BY / LIMIT.
12770 let mut head = self.parse_bare_select()?;
12771 let into = self.pending_select_into.take();
12772 self.parse_setop_chain_into(&mut head)?;
12773 self.parse_select_tail_into(&mut head)?;
12774 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12775 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12776 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12777 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12778 // to the body, as it does in PostgreSQL.
12779 if let Some((name, temporary)) = into {
12780 return Ok(Statement::CreateMaterializedView(
12781 crate::ast::CreateMaterializedViewStatement {
12782 temporary,
12783 name,
12784 if_not_exists: false,
12785 columns: Vec::new(),
12786 body: head,
12787 with_data: true,
12788 as_plain_table: true,
12789 },
12790 ));
12791 }
12792 Ok(Statement::Select(head))
12793 }
12794
12795 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12796 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12797 /// token), and INTERSECT [ALL] (a bare ident — it was never
12798 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12799 /// tighter than UNION / EXCEPT — the executor folds the chain
12800 /// left-to-right, which is already correct for LEADING
12801 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12802 /// pair nests into that previous peer, so A UNION B INTERSECT C
12803 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12804 /// groups.
12805 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12806 // A parenthesized group arrives with its own (already
12807 // regrouped) unions on `head`; only the pairs THIS chain
12808 // appends participate in the precedence regroup below —
12809 // nesting an outer INTERSECT into a group-internal peer
12810 // would dissolve the explicit grouping.
12811 let boundary = head.unions.len();
12812 loop {
12813 let base = match self.peek() {
12814 Token::Union => UnionKind::Distinct,
12815 Token::Except => UnionKind::Except,
12816 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12817 _ => break,
12818 };
12819 self.advance();
12820 let kind = if matches!(self.peek(), Token::All) {
12821 self.advance();
12822 match base {
12823 UnionKind::Distinct => UnionKind::All,
12824 UnionKind::Except => UnionKind::ExceptAll,
12825 _ => UnionKind::IntersectAll,
12826 }
12827 } else {
12828 base
12829 };
12830 let peer = self.parse_bare_select()?;
12831 head.unions.push((kind, peer));
12832 }
12833 let mut pairs = core::mem::take(&mut head.unions);
12834 let tail = pairs.split_off(boundary);
12835 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12836 for (kind, peer) in tail {
12837 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12838 // An intersect nests into the previous element of THIS
12839 // chain only; with no new previous element it stays at
12840 // the outer level (the left fold applies it to the
12841 // whole head, group included).
12842 match (
12843 is_intersect,
12844 regrouped.len() > boundary,
12845 regrouped.last_mut(),
12846 ) {
12847 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12848 _ => regrouped.push((kind, peer)),
12849 }
12850 }
12851 head.unions = regrouped;
12852 Ok(())
12853 }
12854
12855 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12856 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12857 /// the top-level bare VALUES statement reuses it verbatim.
12858 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12859 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12860 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12861 /// where the grouping-set universe is still in scope.
12862 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12863 if !matches!(self.peek(), Token::Order) {
12864 return Ok(Vec::new());
12865 }
12866 self.advance();
12867 if !self.peek_is_by() {
12868 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12869 }
12870 self.advance();
12871 let mut keys = Vec::new();
12872 loop {
12873 // v7.39 (round 691) — save/restore, the discipline this parser
12874 // already uses around `pending_sample_preds`, so a subquery inside
12875 // a key neither inherits nor leaks the channel.
12876 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12877 let saved_coll = self.order_key_collation.take();
12878 let parsed = self.parse_expr(0);
12879 self.in_order_by_key = saved_flag;
12880 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12881 let expr = parsed?;
12882 let desc = if matches!(self.peek(), Token::Desc) {
12883 self.advance();
12884 true
12885 } else if matches!(self.peek(), Token::Asc) {
12886 self.advance();
12887 false
12888 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12889 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12890 // one ordering per type, so the btree comparison operators map
12891 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12892 // would need a custom operator class — honest error.
12893 self.advance();
12894 match self.advance() {
12895 Token::Lt | Token::LtEq => false,
12896 Token::Gt | Token::GtEq => true,
12897 other => {
12898 return Err(self.err(alloc::format!(
12899 "ORDER BY USING supports the btree comparison \
12900 operators (< <= > >=); got {other:?}"
12901 )));
12902 }
12903 }
12904 } else {
12905 false
12906 };
12907 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12908 let nulls_first = self.parse_optional_nulls_placement()?;
12909 keys.push(OrderBy {
12910 expr,
12911 desc,
12912 nulls_first,
12913 collation,
12914 });
12915 if matches!(self.peek(), Token::Comma) {
12916 self.advance();
12917 } else {
12918 break;
12919 }
12920 }
12921 Ok(keys)
12922 }
12923
12924 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12925 // v7.39 (round 135) — a grouping-set query may have already parsed +
12926 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12927 // no ORDER BY token is present, keep that pre-set order_by rather than
12928 // clobbering it with an empty list.
12929 let parsed_keys = self.parse_order_by_keys()?;
12930 head.order_by = if parsed_keys.is_empty() {
12931 core::mem::take(&mut head.order_by)
12932 } else {
12933 parsed_keys
12934 };
12935 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12936 // order. PG's grammar takes a limit clause and an offset clause
12937 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12938 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12939 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12940 // spelling died on `expected end of input, got Limit`.
12941 //
12942 // Each may appear at most once, and LIMIT and FETCH FIRST are
12943 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12944 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12945 // A second one is left unconsumed here, which the caller reports
12946 // as trailing input rather than silently taking the last.
12947 let mut saw_limit = false;
12948 let mut saw_offset = false;
12949 loop {
12950 if !saw_limit && matches!(self.peek(), Token::Limit) {
12951 self.advance();
12952 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12953 // PG synonyms for "no limit". Treat both as None
12954 // (no head.limit set) so the engine's existing
12955 // unlimited-result path takes over. Reject was the
12956 // pre-5.1 behaviour and broke pg_dump-flavoured
12957 // tooling that occasionally emits LIMIT NULL.
12958 if self.consume_limit_unbounded_sentinel() {
12959 head.limit = None;
12960 } else {
12961 let first = self.parse_limit_expr("LIMIT")?;
12962 // MySQL `LIMIT offset, count` — the first number is
12963 // the offset when a comma follows.
12964 if matches!(self.peek(), Token::Comma) {
12965 self.advance();
12966 let count = self.parse_limit_expr("LIMIT")?;
12967 head.offset = Some(first);
12968 saw_offset = true;
12969 head.limit = Some(count);
12970 } else {
12971 head.limit = Some(first);
12972 }
12973 }
12974 saw_limit = true;
12975 continue;
12976 }
12977 if !saw_offset && matches!(self.peek(), Token::Offset) {
12978 self.advance();
12979 // PG also accepts an optional `ROW` / `ROWS` trailer
12980 // after the offset value (`OFFSET 10 ROWS`). The
12981 // FETCH-FIRST branch below relies on the same.
12982 let off = self.parse_limit_expr("OFFSET")?;
12983 self.consume_optional_rows_keyword();
12984 head.offset = Some(off);
12985 saw_offset = true;
12986 continue;
12987 }
12988 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12989 // the SQL-standard alias for LIMIT. PG accepts both
12990 // spellings interchangeably; pg_dump emits FETCH FIRST in
12991 // newer versions. We map it onto `head.limit` so the
12992 // engine path is unified.
12993 if !saw_limit
12994 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12995 if s.eq_ignore_ascii_case("fetch"))
12996 {
12997 self.advance(); // FETCH
12998 // `FIRST` or `NEXT` (both legal per SQL standard).
12999 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13000 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
13001 {
13002 self.advance();
13003 }
13004 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
13005 // implicit 1 — but we always consume one if present).
13006 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13007 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13008 {
13009 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
13010 crate::ast::LimitExpr::Literal(1)
13011 } else {
13012 self.parse_limit_expr("FETCH FIRST")?
13013 };
13014 // Eat `ROW` / `ROWS` if not already consumed above.
13015 self.consume_optional_rows_keyword();
13016 // Optional `ONLY` (the spec form) — or the SQL:2008
13017 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
13018 // now honours WITH TIES by extending past the LIMIT
13019 // truncation point through every row that shares the
13020 // last-kept row's ORDER BY key.
13021 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13022 if s.eq_ignore_ascii_case("only"))
13023 {
13024 self.advance();
13025 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13026 if s.eq_ignore_ascii_case("with"))
13027 {
13028 self.advance(); // WITH
13029 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13030 if s.eq_ignore_ascii_case("ties"))
13031 {
13032 self.advance();
13033 head.limit_with_ties = true;
13034 }
13035 }
13036 head.limit = Some(count);
13037 saw_limit = true;
13038 continue;
13039 }
13040 break;
13041 }
13042 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
13043 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
13044 // [ OF table_name [, …] ]
13045 // [ NOWAIT | SKIP LOCKED ]
13046 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
13047 // FOR SHARE OF t2`). SPG is a single-writer engine — every
13048 // SELECT already returns a consistent snapshot — so these
13049 // are accept-and-discard: the parser absorbs them so
13050 // mailrs / Rails / Django code paths that emit `SELECT
13051 // … FOR UPDATE` for advisory pessimistic locking load
13052 // without a parser error. The on-disk locking model is
13053 // unchanged; callers that rely on FOR UPDATE for read-
13054 // through-write ordering still get the right answer
13055 // because SPG serialises writes anyway.
13056 head.locking = self
13057 .consume_optional_for_lock_clauses()
13058 .map(alloc::boxed::Box::new);
13059 Ok(())
13060 }
13061
13062 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
13063 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
13064 /// LOCKED ]` trailers. Each clause is fully accepted and
13065 /// discarded — SPG's single-writer model already satisfies the
13066 /// callers' implicit ordering requirement. Stops at the first
13067 /// token that isn't `FOR`.
13068 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
13069 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
13070 // not discarded. PG keeps the strongest of several clauses; the
13071 // policy of the last one wins, which is what this loop records.
13072 let mut seen: Option<crate::ast::LockingClause> = None;
13073 while matches!(self.peek(), Token::For) {
13074 // v7.37.14 (A2.5-stub) — record that this query asked
13075 // for a row lock the parser is about to silently
13076 // discard. Operators surface the count via
13077 // `spg_sql::silent_for_update_count()` so they can
13078 // gauge how much of the workload depends on advisory
13079 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13080 // before v7.37.15's per-row tuple locking lands.
13081 crate::record_silent_for_update_clause();
13082 self.advance(); // FOR
13083 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13084 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13085 let mut no_key = false;
13086 let mut key = false;
13087 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13088 if s.eq_ignore_ascii_case("no"))
13089 {
13090 self.advance(); // NO
13091 no_key = true;
13092 // The next ident should be KEY but be generous;
13093 // anything followed by UPDATE/SHARE is accepted.
13094 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13095 if s.eq_ignore_ascii_case("key"))
13096 {
13097 self.advance(); // KEY
13098 }
13099 }
13100 // `KEY` prefix (PG `FOR KEY SHARE`).
13101 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13102 if s.eq_ignore_ascii_case("key"))
13103 {
13104 self.advance(); // KEY
13105 key = true;
13106 }
13107 // Lock-strength keyword: UPDATE / SHARE. Required, but
13108 // we're lenient — an unexpected token here just bails
13109 // (we already consumed FOR; caller's downstream
13110 // dispatch will error if anything actually depends on
13111 // the trailing tokens).
13112 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13113 if s.eq_ignore_ascii_case("update"));
13114 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13115 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13116 {
13117 self.advance();
13118 use crate::ast::LockStrength as LS;
13119 let strength = match (is_update, no_key, key) {
13120 (true, true, _) => LS::NoKeyUpdate,
13121 (true, _, _) => LS::Update,
13122 (false, _, true) => LS::KeyShare,
13123 (false, _, _) => LS::Share,
13124 };
13125 seen = Some(crate::ast::LockingClause {
13126 strength,
13127 of_tables: alloc::vec::Vec::new(),
13128 policy: crate::ast::LockWait::Wait,
13129 });
13130 } else {
13131 // FOR by itself (or `FOR KEY` with nothing after) —
13132 // give up on the lock-clause path. We've already
13133 // advanced past FOR; further attempts to parse
13134 // here would clobber state.
13135 return seen;
13136 }
13137 // Optional `OF tbl[, tbl …]`. mailrs emits this when
13138 // joining and locking only a subset of tables.
13139 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13140 if s.eq_ignore_ascii_case("of"))
13141 {
13142 self.advance(); // OF
13143 #[allow(clippy::while_let_loop)]
13144 loop {
13145 match self.peek() {
13146 Token::Ident(_) | Token::QuotedIdent(_) => {
13147 // v7.39 (round 294) — the name is CAPTURED now: PG
13148 // validates it against the FROM clause, and an
13149 // uncaptured list silently means "lock everything".
13150 let mut nm = match self.advance() {
13151 Token::Ident(n) | Token::QuotedIdent(n) => n,
13152 _ => alloc::string::String::new(),
13153 };
13154 // Optional schema-qualified `schema.table`.
13155 if matches!(self.peek(), Token::Dot) {
13156 self.advance();
13157 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13158 {
13159 self.advance();
13160 nm = n;
13161 }
13162 }
13163 if let Some(c) = seen.as_mut() {
13164 c.of_tables.push(nm);
13165 }
13166 }
13167 _ => break,
13168 }
13169 if matches!(self.peek(), Token::Comma) {
13170 self.advance();
13171 } else {
13172 break;
13173 }
13174 }
13175 }
13176 // Optional `NOWAIT` | `SKIP LOCKED`.
13177 match self.peek().clone() {
13178 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13179 self.advance();
13180 if let Some(c) = seen.as_mut() {
13181 c.policy = crate::ast::LockWait::NoWait;
13182 }
13183 }
13184 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13185 self.advance(); // SKIP
13186 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13187 if s.eq_ignore_ascii_case("locked"))
13188 {
13189 self.advance(); // LOCKED
13190 if let Some(c) = seen.as_mut() {
13191 c.policy = crate::ast::LockWait::SkipLocked;
13192 }
13193 }
13194 }
13195 _ => {}
13196 }
13197 // Loop: PG allows multiple FOR clauses chained.
13198 }
13199 seen
13200 }
13201
13202 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13203 /// Bind value gets resolved during prepared-statement Execute;
13204 /// the Pratt expression parser would over-accept here (e.g.
13205 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13206 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13207 /// sentinel tokens (PG synonyms for "no limit"). Returns true
13208 /// when one was consumed; caller skips the regular
13209 /// limit-value parse and leaves `head.limit` at None.
13210 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13211 if matches!(self.peek(), Token::Null) {
13212 self.advance();
13213 return true;
13214 }
13215 if matches!(self.peek(), Token::All) {
13216 self.advance();
13217 return true;
13218 }
13219 false
13220 }
13221
13222 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13223 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13224 /// SQL-standard shape. No-op when missing.
13225 fn consume_optional_rows_keyword(&mut self) {
13226 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13227 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13228 {
13229 self.advance();
13230 }
13231 }
13232
13233 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13234 ///
13235 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13236 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13237 /// constant, which is why that spelling keeps the token path below.
13238 ///
13239 /// Constants are folded here rather than carried into the tree: the
13240 /// 15+ execution paths that read the row count go through
13241 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13242 /// means "no limit". A clause the engine could not resolve would
13243 /// therefore return the WHOLE table instead of failing. Folding at
13244 /// parse time keeps that impossible; a non-constant clause is still
13245 /// a clean error (recorded residual — closing it wants a resolution
13246 /// pre-pass on the simple-query path, where `substitute_placeholders`
13247 /// does not run).
13248 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13249 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13250 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13251 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13252 // ONLY` both work (its grammar takes a c_expr). Both measured
13253 // against PG 18.4 in round 305.
13254 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13255 return self.parse_limit_constant(label);
13256 }
13257 // One pass, no rewind: `advance()` takes each token by
13258 // `mem::replace`, so a consumed token reads back as Eof and this
13259 // parser cannot backtrack. Everything — bare literal included —
13260 // is therefore folded from the parsed expression rather than
13261 // re-read from the token stream.
13262 let start = self.pos;
13263 let e = self.parse_expr(0)?;
13264 if let crate::ast::Expr::Placeholder(n) = e {
13265 return Ok(crate::ast::LimitExpr::Placeholder(n));
13266 }
13267 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13268 match fold_limit_constant(&e) {
13269 Some(Ok(v)) if v < 0 => Err(ParseError {
13270 message: alloc::format!("{neg_label} must not be negative"),
13271 token_pos: start,
13272 }),
13273 Some(Ok(v)) => u32::try_from(v)
13274 .map(crate::ast::LimitExpr::Literal)
13275 .map_err(|_| ParseError {
13276 message: alloc::format!("{label} value too large: {v}"),
13277 token_pos: start,
13278 }),
13279 Some(Err(message)) => Err(ParseError {
13280 message: message.replace("{L}", neg_label),
13281 token_pos: start,
13282 }),
13283 // v7.39 (round 305, V23) — not foldable at parse time
13284 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13285 // expression; the engine evaluates it once before dispatch.
13286 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13287 }
13288 }
13289
13290 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13291 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13292 // coercion rules, not just an integer token: a NUMERIC rounds half
13293 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13294 // refused with PG's wording ("LIMIT must not be negative", 2201W /
13295 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13296 // content, failing as an input-syntax error on the value. General
13297 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13298 // they need an Expr-carrying LimitExpr variant.
13299 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13300 let err_at = |message: alloc::string::String, pos: usize| ParseError {
13301 message,
13302 token_pos: pos,
13303 };
13304 match self.advance() {
13305 Token::Integer(n) if n >= 0 => u32::try_from(n)
13306 .map(crate::ast::LimitExpr::Literal)
13307 .map_err(|_| ParseError {
13308 message: alloc::format!("{label} value too large: {n}"),
13309 token_pos: self.consumed_pos(),
13310 }),
13311 Token::Integer(_) => Err(err_at(
13312 alloc::format!("{neg_label} must not be negative"),
13313 self.pos.saturating_sub(1),
13314 )),
13315 Token::Numeric(t) => {
13316 let pos = self.pos.saturating_sub(1);
13317 let v: f64 = t.parse().map_err(|_| {
13318 err_at(
13319 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13320 pos,
13321 )
13322 })?;
13323 if v < 0.0 {
13324 return Err(err_at(
13325 alloc::format!("{neg_label} must not be negative"),
13326 pos,
13327 ));
13328 }
13329 // Round half away from zero — PG's numeric→bigint cast.
13330 // (no_std: no f64::round; v is non-negative, so truncating
13331 // v + 0.5 is the same thing.)
13332 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13333 let rounded = (v + 0.5) as u64;
13334 u32::try_from(rounded)
13335 .map(crate::ast::LimitExpr::Literal)
13336 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13337 }
13338 Token::Minus => {
13339 let pos = self.pos.saturating_sub(1);
13340 match self.peek() {
13341 Token::Integer(_) | Token::Numeric(_) => {
13342 self.advance();
13343 Err(err_at(
13344 alloc::format!("{neg_label} must not be negative"),
13345 pos,
13346 ))
13347 }
13348 other => Err(err_at(
13349 alloc::format!(
13350 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13351 ),
13352 pos,
13353 )),
13354 }
13355 }
13356 Token::String(t) => {
13357 let pos = self.pos.saturating_sub(1);
13358 match t.trim().parse::<i64>() {
13359 Ok(n) if n < 0 => Err(err_at(
13360 alloc::format!("{neg_label} must not be negative"),
13361 pos,
13362 )),
13363 Ok(n) => u32::try_from(n)
13364 .map(crate::ast::LimitExpr::Literal)
13365 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13366 Err(_) => Err(err_at(
13367 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13368 pos,
13369 )),
13370 }
13371 }
13372 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13373 other => Err(ParseError {
13374 message: alloc::format!(
13375 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13376 ),
13377 token_pos: self.consumed_pos(),
13378 }),
13379 }
13380 }
13381
13382 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13383 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13384 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13385 /// `parse_select_stmt` is responsible for filling those in.
13386 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13387 /// call in the expression tree to the per-set integer bitmask
13388 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13389 /// is dropped in this grouping set). Runs during the ROLLUP /
13390 /// CUBE / GROUPING SETS expansion, where the set is known.
13391 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13392 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13393 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13394 if let Expr::FunctionCall { name, .. } = expr
13395 && name.eq_ignore_ascii_case("grouping")
13396 {
13397 if !out.iter().any(|e| e == expr) {
13398 out.push(expr.clone());
13399 }
13400 return;
13401 }
13402 match expr {
13403 Expr::Binary { lhs, rhs, .. } => {
13404 Self::collect_grouping_calls(lhs, out);
13405 Self::collect_grouping_calls(rhs, out);
13406 }
13407 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13408 Self::collect_grouping_calls(expr, out)
13409 }
13410 Expr::FunctionCall { args, .. } => {
13411 for a in args {
13412 Self::collect_grouping_calls(a, out);
13413 }
13414 }
13415 Expr::Case {
13416 operand,
13417 branches,
13418 else_branch,
13419 } => {
13420 if let Some(o) = operand {
13421 Self::collect_grouping_calls(o, out);
13422 }
13423 for (c, v) in branches {
13424 Self::collect_grouping_calls(c, out);
13425 Self::collect_grouping_calls(v, out);
13426 }
13427 if let Some(x) = else_branch {
13428 Self::collect_grouping_calls(x, out);
13429 }
13430 }
13431 _ => {}
13432 }
13433 }
13434
13435 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13436 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13437 /// `__grp_ord_k` (injected per grouping-set branch).
13438 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13439 if let Expr::FunctionCall { name, .. } = expr
13440 && name.eq_ignore_ascii_case("grouping")
13441 {
13442 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13443 *expr = Expr::Column(crate::ast::ColumnName {
13444 qualifier: None,
13445 name: alloc::format!("__grp_ord_{k}"),
13446 });
13447 }
13448 return;
13449 }
13450 match expr {
13451 Expr::Binary { lhs, rhs, .. } => {
13452 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13453 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13454 }
13455 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13456 Self::rewrite_grouping_to_col(expr, grp_exprs)
13457 }
13458 Expr::FunctionCall { args, .. } => {
13459 for a in args {
13460 Self::rewrite_grouping_to_col(a, grp_exprs);
13461 }
13462 }
13463 Expr::Case {
13464 operand,
13465 branches,
13466 else_branch,
13467 } => {
13468 if let Some(o) = operand {
13469 Self::rewrite_grouping_to_col(o, grp_exprs);
13470 }
13471 for (c, v) in branches {
13472 Self::rewrite_grouping_to_col(c, grp_exprs);
13473 Self::rewrite_grouping_to_col(v, grp_exprs);
13474 }
13475 if let Some(x) = else_branch {
13476 Self::rewrite_grouping_to_col(x, grp_exprs);
13477 }
13478 }
13479 _ => {}
13480 }
13481 }
13482
13483 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13484 /// as the list of key sets it contributes. A bare expression is one
13485 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13486 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13487 /// the concatenation of its items' sets, where an item is itself an
13488 /// element, a parenthesized key list, or the empty set `()`. A
13489 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13490 /// move together.
13491 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13492 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13493 // ROLLUP ( … ) / CUBE ( … )
13494 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13495 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13496 {
13497 let is_cube = is_kw(self.peek(), "cube");
13498 self.advance(); // ROLLUP / CUBE
13499 self.advance(); // (
13500 let mut units: Vec<Vec<Expr>> = Vec::new();
13501 loop {
13502 if matches!(self.peek(), Token::LParen) {
13503 // Composite unit: (a, b) rolls up as one.
13504 self.advance();
13505 let mut unit = Vec::new();
13506 if !matches!(self.peek(), Token::RParen) {
13507 loop {
13508 unit.push(self.parse_expr(0)?);
13509 match self.peek() {
13510 Token::Comma => {
13511 self.advance();
13512 }
13513 Token::RParen => break,
13514 other => {
13515 return Err(self.err(format!(
13516 "expected ',' or ')' in grouping unit, got {other:?}"
13517 )));
13518 }
13519 }
13520 }
13521 }
13522 self.advance(); // )
13523 units.push(unit);
13524 } else {
13525 units.push(alloc::vec![self.parse_expr(0)?]);
13526 }
13527 match self.peek() {
13528 Token::Comma => {
13529 self.advance();
13530 }
13531 Token::RParen => break,
13532 other => {
13533 return Err(self.err(format!(
13534 "expected ',' or ')' in grouping list, got {other:?}"
13535 )));
13536 }
13537 }
13538 }
13539 self.advance(); // )
13540 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13541 units
13542 .iter()
13543 .zip(unit_sel.iter())
13544 .filter(|(_, keep)| **keep)
13545 .flat_map(|(u, _)| u.iter().cloned())
13546 .collect()
13547 };
13548 let n = units.len();
13549 if is_cube {
13550 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13551 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13552 .collect();
13553 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13554 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13555 }
13556 return Ok((0..=n)
13557 .rev()
13558 .map(|keep| {
13559 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13560 flatten(&sel)
13561 })
13562 .collect());
13563 }
13564 // GROUPING SETS ( item [, item]* )
13565 if is_kw(self.peek(), "grouping")
13566 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13567 {
13568 self.advance(); // GROUPING
13569 self.advance(); // SETS
13570 if !matches!(self.peek(), Token::LParen) {
13571 return Err(self.err(format!(
13572 "expected '(' after GROUPING SETS, got {:?}",
13573 self.peek()
13574 )));
13575 }
13576 self.advance(); // outer (
13577 let mut sets: Vec<Vec<Expr>> = Vec::new();
13578 loop {
13579 if matches!(self.peek(), Token::LParen) {
13580 // A parenthesized key list (or the empty set).
13581 self.advance();
13582 let mut set = Vec::new();
13583 if !matches!(self.peek(), Token::RParen) {
13584 loop {
13585 set.push(self.parse_expr(0)?);
13586 match self.peek() {
13587 Token::Comma => {
13588 self.advance();
13589 }
13590 Token::RParen => break,
13591 other => {
13592 return Err(self.err(format!(
13593 "expected ',' or ')' in grouping set, got {other:?}"
13594 )));
13595 }
13596 }
13597 }
13598 }
13599 self.advance(); // )
13600 sets.push(set);
13601 } else {
13602 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13603 // bare expression.
13604 sets.extend(self.parse_grouping_element()?);
13605 }
13606 match self.peek() {
13607 Token::Comma => {
13608 self.advance();
13609 }
13610 Token::RParen => break,
13611 other => {
13612 return Err(self.err(format!(
13613 "expected ',' or ')' after a grouping set, got {other:?}"
13614 )));
13615 }
13616 }
13617 }
13618 self.advance(); // outer )
13619 return Ok(sets);
13620 }
13621 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13622 }
13623
13624 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13625 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13626 // set evaluates to NULL, at any depth. Previously only a *top-level*
13627 // select item equal to a dropped key was nullified, so a key nested in
13628 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13629 // column and failed to resolve against the set's synthetic schema.
13630 if dropped.iter().any(|d| d == expr) {
13631 *expr = Expr::Literal(Literal::Null);
13632 return;
13633 }
13634 if let Expr::FunctionCall { name, args } = expr
13635 && name.eq_ignore_ascii_case("grouping")
13636 {
13637 let mut mask: i64 = 0;
13638 for a in args.iter() {
13639 mask <<= 1;
13640 if dropped.iter().any(|d| d == a) {
13641 mask |= 1;
13642 }
13643 }
13644 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13645 // literal: a bare integer in a select item is indistinguishable
13646 // from a positional reference once `ORDER BY 1` substitutes the
13647 // item back in, and the round-232 position check then read the
13648 // mask value as an out-of-range position. The cast changes
13649 // nothing semantically (grouping() is integer).
13650 *expr = Expr::Cast {
13651 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13652 target: crate::ast::CastTarget::Int,
13653 };
13654 return;
13655 }
13656 // Generic recursion over the common expression shapes the
13657 // SELECT list uses; anything without child expressions is
13658 // left alone.
13659 match expr {
13660 // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13661 //
13662 // A grouping column is NULL in the OUTPUT of a set that
13663 // drops it, and an aggregate over it still aggregates the
13664 // real values. Measured, over 0,1,2,3:
13665 //
13666 // ```text
13667 // SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13668 // PostgreSQL 18.6 the total row is NULL | 6
13669 // MySQL 9.7.2 the total row is NULL | 6
13670 // SPG 7.39.13 NULL | NULL
13671 // ```
13672 //
13673 // The round that made this walk descend "at any depth" was
13674 // right about `COALESCE(g,'TOTAL')` and wrong about
13675 // `SUM(g)`: it turned the aggregate's own input into a NULL
13676 // literal, so the grand total of a rollup keyed on the
13677 // summed column answered nothing. Wrong on BOTH faces.
13678 //
13679 // `grouping(…)` is settled above, before this, so it keeps
13680 // reading the dropped set.
13681 Expr::FunctionCall { name, args } => {
13682 if is_aggregate_function_name(name) {
13683 return;
13684 }
13685 for a in args {
13686 Self::substitute_grouping_calls(a, dropped);
13687 }
13688 }
13689 Expr::AggregateOrdered { .. } => {}
13690 Expr::Binary { lhs, rhs, .. } => {
13691 Self::substitute_grouping_calls(lhs, dropped);
13692 Self::substitute_grouping_calls(rhs, dropped);
13693 }
13694 Expr::Unary { expr: inner, .. } => {
13695 Self::substitute_grouping_calls(inner, dropped);
13696 }
13697 Expr::Cast { expr: inner, .. } => {
13698 Self::substitute_grouping_calls(inner, dropped);
13699 }
13700 Expr::Case {
13701 operand,
13702 branches,
13703 else_branch,
13704 } => {
13705 if let Some(op) = operand {
13706 Self::substitute_grouping_calls(op, dropped);
13707 }
13708 for (w, t) in branches {
13709 Self::substitute_grouping_calls(w, dropped);
13710 Self::substitute_grouping_calls(t, dropped);
13711 }
13712 if let Some(e) = else_branch {
13713 Self::substitute_grouping_calls(e, dropped);
13714 }
13715 }
13716 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13717 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13718 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13719 // …` is the canonical rollup-total label idiom).
13720 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13721 Expr::Like { expr, pattern, .. } => {
13722 Self::substitute_grouping_calls(expr, dropped);
13723 Self::substitute_grouping_calls(pattern, dropped);
13724 }
13725 Expr::InList { expr, list, .. } => {
13726 Self::substitute_grouping_calls(expr, dropped);
13727 for item in list {
13728 Self::substitute_grouping_calls(item, dropped);
13729 }
13730 }
13731 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13732 Expr::Array(items) => {
13733 for item in items {
13734 Self::substitute_grouping_calls(item, dropped);
13735 }
13736 }
13737 Expr::ArraySubscript { target, index } => {
13738 Self::substitute_grouping_calls(target, dropped);
13739 Self::substitute_grouping_calls(index, dropped);
13740 }
13741 Expr::ArraySlice { target, lo, hi } => {
13742 Self::substitute_grouping_calls(target, dropped);
13743 if let Some(lo) = lo {
13744 Self::substitute_grouping_calls(lo, dropped);
13745 }
13746 if let Some(hi) = hi {
13747 Self::substitute_grouping_calls(hi, dropped);
13748 }
13749 }
13750 Expr::AnyAll { expr, array, .. } => {
13751 Self::substitute_grouping_calls(expr, dropped);
13752 Self::substitute_grouping_calls(array, dropped);
13753 }
13754 _ => {}
13755 }
13756 }
13757
13758 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13759 // v7.40.11 — an UNPARENTHESISED `VALUES` list is a query block
13760 // too, so it can be the PEER of a set operation:
13761 //
13762 // SELECT 1 UNION ALL VALUES (2)
13763 //
13764 // The parenthesised form has been a peer since v7.37 D.20 and
13765 // the CTE-body form since 17.6; these two unbracketed positions
13766 // were the pair nobody wrote a case for. Modern psql builds its
13767 // describe queries with `UNION ALL VALUES`, so every backslash
13768 // command — `\d`, `\dt`, `\di` — failed with a syntax error
13769 // pointing into a query the user did not write.
13770 if matches!(self.peek(), Token::Values) {
13771 self.advance(); // VALUES
13772 return self.parse_values_rows_body();
13773 }
13774 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13775 // group: `( <select chain> )` usable anywhere a query block
13776 // is (head or peer of an outer chain). The group's own
13777 // unions ride the returned SelectStatement; the executor's
13778 // nested-peer recursion runs them.
13779 if matches!(self.peek(), Token::LParen)
13780 && matches!(
13781 self.tokens.get(self.pos + 1),
13782 Some(Token::Select | Token::LParen | Token::Values)
13783 )
13784 {
13785 self.advance(); // (
13786 self.enter_nested()?;
13787 // v7.37 D.20 — a group whose head is a VALUES list:
13788 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13789 // otherwise recurse into a nested SELECT/group head.
13790 let mut head = (if matches!(self.peek(), Token::Values) {
13791 self.advance(); // VALUES
13792 self.parse_values_rows_body()
13793 } else {
13794 self.parse_bare_select()
13795 })
13796 .and_then(|mut h| {
13797 self.parse_setop_chain_into(&mut h)?;
13798 Ok(h)
13799 });
13800 self.nest_depth -= 1;
13801 let mut head = match &mut head {
13802 Ok(h) => core::mem::take(h),
13803 Err(_) => return head,
13804 };
13805 // v7.37.17 (17.6 siblings) — group-internal tail:
13806 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13807 // group head, then wrap the group as a derived table
13808 // (SELECT * FROM (group)) so the outer chain / outer
13809 // tail can't clobber the group's own ordering or limit.
13810 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13811 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13812 if s.eq_ignore_ascii_case("fetch"));
13813 if has_tail {
13814 self.parse_select_tail_into(&mut head)?;
13815 head = SelectStatement {
13816 locking: None,
13817 ctes: Vec::new(),
13818 distinct: false,
13819 distinct_on: Vec::new(),
13820 items: alloc::vec![SelectItem::Wildcard],
13821 from: Some(FromClause {
13822 primary: TableRef {
13823 name: "subquery".to_string(),
13824 alias: None,
13825 only: false,
13826 as_of_segment: None,
13827 unnest_expr: None,
13828 unnest_column_aliases: Vec::new(),
13829 with_ordinality: false,
13830 generate_series_args: None,
13831 lateral_subquery: Some(Box::new(head)),
13832 jsonb_each_text_arg: None,
13833 table_fn_call: None,
13834 rows_from: None,
13835 json_table: None,
13836 scalar_fn_item: false,
13837 },
13838 joins: Vec::new(),
13839 }),
13840 where_: None,
13841 group_by: None,
13842 group_by_all: false,
13843 having: None,
13844 unions: Vec::new(),
13845 order_by: Vec::new(),
13846 limit: None,
13847 offset: None,
13848 limit_with_ties: false,
13849 window_check_exprs: Vec::new(),
13850 };
13851 }
13852 if !matches!(self.peek(), Token::RParen) {
13853 return Err(self.err(format!(
13854 "expected ')' after parenthesized query group, got {:?}",
13855 self.peek()
13856 )));
13857 }
13858 self.advance();
13859 return Ok(head);
13860 }
13861 // `TABLE name` shorthand as a query block — valid anywhere
13862 // a SELECT head is (set-op peers included).
13863 if matches!(self.peek(), Token::Table)
13864 && matches!(
13865 self.tokens.get(self.pos + 1),
13866 Some(Token::Ident(_) | Token::QuotedIdent(_))
13867 )
13868 {
13869 return self.parse_table_shorthand();
13870 }
13871 if !matches!(self.peek(), Token::Select) {
13872 return Err(self.err(format!(
13873 "expected SELECT to start a query block, got {:?}",
13874 self.peek()
13875 )));
13876 }
13877 self.advance();
13878 // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13879 //
13880 // It sits where `DISTINCT` sits and tells the optimiser to join
13881 // in the written order. SPG plans its own joins, so the hint is
13882 // accepted and not acted on — but it has to PARSE, because as a
13883 // bare identifier it became a column: measured on the published
13884 // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13885 // column 'straight_join' in 'field list'` where MySQL 9.7.2
13886 // returns the rows. Only in this position, which is the only one
13887 // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13888 if self.mysql_dialect
13889 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13890 {
13891 self.advance();
13892 }
13893 let distinct = if matches!(self.peek(), Token::Distinct) {
13894 self.advance();
13895 true
13896 } else {
13897 false
13898 };
13899 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13900 // keep the first row (per ORDER BY) of each group the
13901 // expressions define. Django's .distinct('field') shape.
13902 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13903 self.advance(); // ON
13904 if !matches!(self.peek(), Token::LParen) {
13905 return Err(self.err(format!(
13906 "expected '(' after DISTINCT ON, got {:?}",
13907 self.peek()
13908 )));
13909 }
13910 self.advance();
13911 let mut exprs = Vec::new();
13912 loop {
13913 exprs.push(self.parse_expr(0)?);
13914 match self.peek() {
13915 Token::Comma => {
13916 self.advance();
13917 }
13918 Token::RParen => break,
13919 other => {
13920 return Err(self.err(format!(
13921 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13922 )));
13923 }
13924 }
13925 }
13926 self.advance(); // )
13927 exprs
13928 } else {
13929 Vec::new()
13930 };
13931 let mut items = self.parse_select_list()?;
13932 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13933 // of CTAS. It sits exactly here in PG's grammar, right after the
13934 // target list.
13935 //
13936 // A comment in `ast.rs` has said since v7.38 that CTAS and
13937 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13938 // `SELECT i INTO t FROM src` answered `syntax error at or near
13939 // "INTO"`, which the differential found while measuring what
13940 // PostgreSQL tags each of the five materialising forms with. A
13941 // comment describing a capability the code does not have is the
13942 // defect this version has been finding all day, and this is the
13943 // one it found in the parser.
13944 //
13945 // `INTO` is captured rather than consumed here: the name has to
13946 // travel out of a function that returns a `SelectStatement`, and
13947 // the caller lowers the whole thing to the CTAS node.
13948 if matches!(self.peek(), Token::Into) {
13949 self.advance();
13950 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13951 // the target, not part of its name. SPG has one storage
13952 // class, so `UNLOGGED` is accepted and means nothing, which
13953 // is what it already means on `CREATE TABLE`.
13954 let mut temporary = false;
13955 loop {
13956 match self.peek().clone() {
13957 Token::Ident(w) | Token::QuotedIdent(w)
13958 if w.eq_ignore_ascii_case("temp")
13959 || w.eq_ignore_ascii_case("temporary") =>
13960 {
13961 temporary = true;
13962 self.advance();
13963 }
13964 Token::Ident(w) | Token::QuotedIdent(w)
13965 if w.eq_ignore_ascii_case("unlogged") =>
13966 {
13967 self.advance();
13968 }
13969 Token::Table => {
13970 self.advance();
13971 }
13972 _ => break,
13973 }
13974 }
13975 let name = match self.peek().clone() {
13976 Token::Ident(w) | Token::QuotedIdent(w) => {
13977 self.advance();
13978 w
13979 }
13980 other => {
13981 return Err(self.err(alloc::format!(
13982 "expected a table name after SELECT … INTO, got {other:?}"
13983 )));
13984 }
13985 };
13986 self.pending_select_into = Some((name, temporary));
13987 }
13988 // Scope the TABLESAMPLE lowering channel to this SELECT:
13989 // stash whatever an enclosing select accumulated, collect
13990 // our own FROM's predicates, restore after the combine.
13991 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13992 let mut from = if matches!(self.peek(), Token::From) {
13993 self.advance();
13994 Some(self.parse_from_clause()?)
13995 } else {
13996 None
13997 };
13998 // v7.37 D.22 — a set-returning function in the projection with no FROM
13999 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
14000 // rows. Move the first SRF projection item to a FROM-position derived
14001 // table and replace it in the projection with a reference to its output
14002 // column; sibling scalar columns repeat per SRF row. PG names the output
14003 // column after the function (or its AS alias). Reuses the FROM-SRF
14004 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
14005 // works via the targetlist-SRF path.
14006 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
14007 // `SELECT * FROM f(args)` — the record's fields become the columns, which
14008 // is exactly what the function's own row shape already is. Anywhere else
14009 // (per outer row, or beside other items) it would need a real record-typed
14010 // projection, so it says so rather than answering something else.
14011 if let [
14012 SelectItem::Expr {
14013 expr: Expr::FunctionCall { name, args },
14014 ..
14015 },
14016 ] = items.as_slice()
14017 && name == "__record_expand"
14018 {
14019 let Some(Expr::FunctionCall {
14020 name: inner_name,
14021 args: inner_args,
14022 }) = args.first()
14023 else {
14024 return Err(self.err(
14025 "(<expr>).* expands a function's record — it needs a function call".into(),
14026 ));
14027 };
14028 if from.is_some() {
14029 return Err(self.err(
14030 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
14031 .into(),
14032 ));
14033 }
14034 let fn_ref = TableRef {
14035 name: inner_name.clone(),
14036 alias: None,
14037 only: false,
14038 as_of_segment: None,
14039 unnest_expr: None,
14040 unnest_column_aliases: Vec::new(),
14041 with_ordinality: false,
14042 generate_series_args: None,
14043 lateral_subquery: None,
14044 jsonb_each_text_arg: None,
14045 table_fn_call: Some(Box::new((
14046 inner_name.to_ascii_lowercase(),
14047 inner_args.clone(),
14048 ))),
14049 rows_from: None,
14050 json_table: None,
14051 scalar_fn_item: false,
14052 };
14053 items = alloc::vec![SelectItem::Wildcard];
14054 from = Some(FromClause {
14055 primary: fn_ref,
14056 joins: Vec::new(),
14057 });
14058 }
14059 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
14060 // FROM, keeps its marker: the ENGINE lowers it, because naming the
14061 // record's fields takes the catalog. It becomes a LATERAL of the same
14062 // function plus one item per declared column — the machinery rounds 65
14063 // and 69 already built.
14064 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
14065 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
14066 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
14067 // express, since the lifted one becomes a scan and the other would
14068 // expand per its rows (a cross product, not a zip). So when the
14069 // projection holds more than one top-level function call, the lift steps
14070 // aside and the engine's target-list expansion takes the whole list.
14071 let fn_call_items = items
14072 .iter()
14073 .filter(|it| {
14074 matches!(
14075 it,
14076 SelectItem::Expr {
14077 expr: Expr::FunctionCall { .. },
14078 ..
14079 }
14080 )
14081 })
14082 .count();
14083 if from.is_none() && fn_call_items <= 1 {
14084 let mut found: Option<(usize, TableRef, String)> = None;
14085 for (i, item) in items.iter().enumerate() {
14086 if let SelectItem::Expr {
14087 expr: Expr::FunctionCall { name, args },
14088 alias,
14089 } = item
14090 {
14091 let lname = name.to_ascii_lowercase();
14092 let colname = alias.clone().unwrap_or_else(|| lname.clone());
14093 let (unnest, gs) = match lname.as_str() {
14094 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14095 "generate_series" if (2..=3).contains(&args.len()) => {
14096 (None, Some(args.clone()))
14097 }
14098 // v7.38 (read01) — generate_subscripts(arr, dim) in a
14099 // no-FROM projection yields the 1-based subscripts, i.e.
14100 // generate_series(1, array_length(arr, dim)); an invalid
14101 // dimension makes array_length NULL → 0 rows, as in PG.
14102 "generate_subscripts" if args.len() == 2 => (
14103 None,
14104 Some(alloc::vec![
14105 Expr::Literal(Literal::Integer(1)),
14106 Expr::FunctionCall {
14107 name: "array_length".to_string(),
14108 args: args.clone(),
14109 },
14110 ]),
14111 ),
14112 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14113 // in a no-FROM projection unnest their *_to_array form.
14114 "string_to_table" | "regexp_split_to_table" => {
14115 let array_fn = if lname == "string_to_table" {
14116 "string_to_array"
14117 } else {
14118 "regexp_split_to_array"
14119 };
14120 (
14121 Some(Box::new(Expr::FunctionCall {
14122 name: array_fn.to_string(),
14123 args: args.clone(),
14124 })),
14125 None,
14126 )
14127 }
14128 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14129 // a no-FROM projection expand per element. The scalar form
14130 // returns the elements as a TEXT array, so unnest over the
14131 // same call materialises one row each (same rewrite the
14132 // FROM-clause form uses).
14133 "jsonb_array_elements"
14134 | "json_array_elements"
14135 | "jsonb_array_elements_text"
14136 | "json_array_elements_text"
14137 if args.len() == 1 =>
14138 {
14139 (
14140 Some(Box::new(Expr::FunctionCall {
14141 name: lname.clone(),
14142 args: args.clone(),
14143 })),
14144 None,
14145 )
14146 }
14147 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14148 // in a no-FROM projection expands per match (scalar form
14149 // returns the matches as a TEXT array → unnest).
14150 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14151 Some(Box::new(Expr::FunctionCall {
14152 name: lname.clone(),
14153 args: args.clone(),
14154 })),
14155 None,
14156 ),
14157 _ => continue,
14158 };
14159 found = Some((
14160 i,
14161 TableRef {
14162 name: colname.clone(),
14163 alias: Some(colname.clone()),
14164 only: false,
14165 as_of_segment: None,
14166 unnest_expr: unnest,
14167 unnest_column_aliases: alloc::vec![colname.clone()],
14168 with_ordinality: false,
14169 generate_series_args: gs,
14170 lateral_subquery: None,
14171 jsonb_each_text_arg: None,
14172 table_fn_call: None,
14173 rows_from: None,
14174 json_table: None,
14175 scalar_fn_item: false,
14176 },
14177 colname,
14178 ));
14179 break;
14180 }
14181 }
14182 if let Some((idx, tref, colname)) = found {
14183 from = Some(FromClause {
14184 primary: tref,
14185 joins: Vec::new(),
14186 });
14187 items[idx] = SelectItem::Expr {
14188 expr: Expr::Column(ColumnName {
14189 qualifier: None,
14190 name: colname.clone(),
14191 }),
14192 alias: Some(colname),
14193 };
14194 }
14195 }
14196 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14197 let where_ = if matches!(self.peek(), Token::Where) {
14198 self.advance();
14199 Some(self.parse_expr(0)?)
14200 } else {
14201 None
14202 };
14203 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14204 Some(match acc {
14205 Some(w) => Expr::Binary {
14206 lhs: Box::new(pred),
14207 op: crate::ast::BinOp::And,
14208 rhs: Box::new(w),
14209 },
14210 None => pred,
14211 })
14212 });
14213 self.pending_sample_preds = enclosing_sample_preds;
14214 let mut group_by_all = false;
14215 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14216 // share one expansion: `grouping_sets` lists the key subsets
14217 // (first = primary, assigned to stmt.group_by; the rest
14218 // become UNION ALL peers), `grouping_universe` is the full
14219 // key list used to compute each peer's dropped keys.
14220 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14221 let mut grouping_universe: Vec<Expr> = Vec::new();
14222 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14223 // A BOOL, not the key list: this frame is the statement parser's, and
14224 // round 430 measured that a `Vec` local here is enough on its own to
14225 // tip the 512 KiB nesting guard. The keys are recoverable from
14226 // `grouping_universe`, which a rollup fills with exactly them.
14227 let mut mysql_rollup = false;
14228 let group_by = if matches!(self.peek(), Token::Group) {
14229 self.advance();
14230 if !self.peek_is_by() {
14231 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14232 }
14233 self.advance();
14234 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14235 // every non-aggregate SELECT-list item later.
14236 if matches!(self.peek(), Token::All) {
14237 self.advance();
14238 group_by_all = true;
14239 None
14240 } else {
14241 // v7.39 (round 242) — PG's general grouping-element grammar:
14242 // GROUP BY [DISTINCT] element [, element]*, where an element
14243 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14244 // SETS (…) — mixed freely. Each element yields a list of
14245 // key sets; the query's grouping sets are the CARTESIAN
14246 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14247 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14248 // content. ROLLUP/CUBE members may be composite
14249 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14250 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14251 // parser handled only a lone ROLLUP/CUBE/GS as the whole
14252 // clause.
14253 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14254 self.advance();
14255 true
14256 } else {
14257 false
14258 };
14259 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14260 loop {
14261 element_sets.push(self.parse_grouping_element()?);
14262 if matches!(self.peek(), Token::Comma) {
14263 self.advance();
14264 } else {
14265 break;
14266 }
14267 }
14268 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14269 for el in &element_sets {
14270 let mut next: Vec<Vec<Expr>> = Vec::new();
14271 for base in &total {
14272 for set in el {
14273 let mut merged = base.clone();
14274 for k in set {
14275 if !merged.iter().any(|m| m == k) {
14276 merged.push(k.clone());
14277 }
14278 }
14279 next.push(merged);
14280 }
14281 }
14282 total = next;
14283 }
14284 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14285 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14286 // The keys and the aggregates come out identical; the ROW
14287 // ORDER does not, and that is the part a report depends on.
14288 // MySQL interleaves each group's subtotal right after its
14289 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14290 // where the union-of-grouping-sets expansion emits every
14291 // leaf first and then every subtotal. MariaDB REFUSES an
14292 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14293 // order itself — measured on MariaDB 11 and MySQL 9.7, which
14294 // agree on the order and disagree only on whether ORDER BY
14295 // is allowed (MySQL allows it; SPG allows it too, since
14296 // refusing would break the clients that can write it).
14297 if self.mysql_dialect
14298 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14299 && matches!(
14300 self.tokens.get(self.pos + 1),
14301 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14302 )
14303 {
14304 self.advance(); // WITH
14305 self.advance(); // ROLLUP
14306 let keys = total.into_iter().next().unwrap_or_default();
14307 mysql_rollup = true;
14308 // n+1 prefixes, largest first — the same expansion
14309 // `ROLLUP (…)` produces.
14310 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14311 }
14312 if distinct_sets {
14313 let mut seen: Vec<Vec<String>> = Vec::new();
14314 total.retain(|set| {
14315 let mut key: Vec<String> =
14316 set.iter().map(|e| alloc::format!("{e}")).collect();
14317 key.sort();
14318 if seen.contains(&key) {
14319 false
14320 } else {
14321 seen.push(key);
14322 true
14323 }
14324 });
14325 }
14326 if total.len() > 1 {
14327 let mut universe: Vec<Expr> = Vec::new();
14328 for set in &total {
14329 for k in set {
14330 if !universe.iter().any(|u| u == k) {
14331 universe.push(k.clone());
14332 }
14333 }
14334 }
14335 grouping_universe = universe;
14336 let primary = total[0].clone();
14337 grouping_sets = total;
14338 Some(primary)
14339 } else {
14340 // One set (a plain GROUP BY list, or a single-set
14341 // spelling like GROUPING SETS ((a, b))). An EMPTY
14342 // single set — GROUPING SETS (()) — stays
14343 // `Some(vec![])`: the grand-total group, which must
14344 // run the aggregate path.
14345 Some(total.into_iter().next().unwrap_or_default())
14346 }
14347 }
14348 } else {
14349 None
14350 };
14351 let having = if matches!(self.peek(), Token::Having) {
14352 self.advance();
14353 Some(self.parse_expr(0)?)
14354 } else {
14355 None
14356 };
14357 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14358 // OVER w parsed to a marker above; inline each definition
14359 // into the referencing WindowFunction nodes.
14360 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14361 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14362 self.advance();
14363 loop {
14364 let wname = self.expect_ident_like()?;
14365 if !matches!(self.peek(), Token::As) {
14366 return Err(self.err(format!(
14367 "expected AS after WINDOW {wname}, got {:?}",
14368 self.peek()
14369 )));
14370 }
14371 self.advance();
14372 // v7.39 (round 229) — PG rejects a redefinition outright.
14373 if window_defs
14374 .iter()
14375 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14376 {
14377 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14378 }
14379 let def = self.parse_over_clause()?;
14380 // A definition may itself copy an earlier one
14381 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14382 // so resolve it against the defs already in scope. Same
14383 // copy rules as an `OVER (w1 …)` in the select list.
14384 let mut probe = Expr::WindowFunction {
14385 name: String::new(),
14386 args: Vec::new(),
14387 partition_by: def.0,
14388 order_by: def.1,
14389 frame: def.2,
14390 null_treatment: crate::ast::NullTreatment::Respect,
14391 filter: None,
14392 };
14393 Self::substitute_named_windows(&mut probe, &window_defs)
14394 .map_err(|m| self.err(m))?;
14395 let Expr::WindowFunction {
14396 partition_by,
14397 order_by,
14398 frame,
14399 ..
14400 } = probe
14401 else {
14402 unreachable!("probe is a WindowFunction")
14403 };
14404 window_defs.push((wname, (partition_by, order_by, frame)));
14405 if matches!(self.peek(), Token::Comma) {
14406 self.advance();
14407 continue;
14408 }
14409 break;
14410 }
14411 }
14412 // v7.39 (round 705) — which definitions did anything reference?
14413 // The ones nothing did used to be dropped here, unexamined, so
14414 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14415 // definition whether referenced or not. Their key expressions ride
14416 // out on the statement for the engine to resolve.
14417 let mut window_refs: Vec<String> = Vec::new();
14418 if !window_defs.is_empty() {
14419 for it in &items {
14420 if let SelectItem::Expr { expr, .. } = it {
14421 Self::collect_named_window_refs(expr, &mut window_refs);
14422 }
14423 }
14424 }
14425 let window_check_exprs: Vec<Expr> = window_defs
14426 .iter()
14427 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14428 .flat_map(|(_, (partition, order, _))| {
14429 partition
14430 .iter()
14431 .cloned()
14432 .chain(order.iter().map(|(e, _, _)| e.clone()))
14433 })
14434 .collect();
14435 if !window_defs.is_empty()
14436 || items
14437 .iter()
14438 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14439 {
14440 for it in &mut items {
14441 if let SelectItem::Expr { expr, .. } = it {
14442 Self::substitute_named_windows(expr, &window_defs)
14443 .map_err(|m| self.err(m))?;
14444 }
14445 }
14446 }
14447 // `GROUP BY 1` — positional keys substitute with the Nth
14448 // select item's expression (same contract ORDER BY has had
14449 // since v6.x). Out-of-range positions error.
14450 let group_by = match group_by {
14451 Some(mut keys) => {
14452 for k in &mut keys {
14453 if let Expr::Literal(Literal::Integer(n)) = k {
14454 let idx = *n;
14455 if idx < 1 || idx as usize > items.len() {
14456 return Err(self.err(alloc::format!(
14457 "GROUP BY position {idx} is not in select list"
14458 )));
14459 }
14460 match &items[(idx - 1) as usize] {
14461 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14462 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14463 return Err(self.err(alloc::format!(
14464 "GROUP BY position {idx} references a wildcard item"
14465 )));
14466 }
14467 }
14468 }
14469 }
14470 Some(keys)
14471 }
14472 None => None,
14473 };
14474 let mut stmt = SelectStatement {
14475 locking: None,
14476 ctes: Vec::new(),
14477 distinct,
14478 distinct_on,
14479 items,
14480 from,
14481 where_,
14482 group_by,
14483 group_by_all,
14484 having,
14485 unions: Vec::new(),
14486 order_by: Vec::new(),
14487 limit: None,
14488 offset: None,
14489 limit_with_ties: false,
14490 window_check_exprs,
14491 };
14492 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14493 // first set is the primary (already on stmt.group_by); each
14494 // further set becomes a UNION ALL peer with its dropped
14495 // keys (universe minus the set) replaced by NULL literals
14496 // in the peer's items and group_by. PG-legal: non-grouped
14497 // select items must be group keys or aggregates, so a
14498 // dropped key's occurrences in the projection are exactly
14499 // the ones to nullify.
14500 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14501 // over a plain GROUP BY (every argument must be a group key; the
14502 // mask is then 0) and rejects anything else with 42803. SPG's
14503 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14504 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14505 // function `grouping`".
14506 if grouping_sets.len() <= 1 {
14507 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14508 let mut calls: Vec<Expr> = Vec::new();
14509 for item in &stmt.items {
14510 if let SelectItem::Expr { expr, .. } = item {
14511 Self::collect_grouping_calls(expr, &mut calls);
14512 }
14513 }
14514 if let Some(h) = &stmt.having {
14515 Self::collect_grouping_calls(h, &mut calls);
14516 }
14517 for call in &calls {
14518 let Expr::FunctionCall { args, .. } = call else {
14519 continue;
14520 };
14521 for a in args {
14522 if !keys.iter().any(|k| k == a) {
14523 return Err(self.err(
14524 "arguments to GROUPING must be grouping expressions of the associated query level"
14525 .to_string(),
14526 ));
14527 }
14528 }
14529 }
14530 if !calls.is_empty() {
14531 for item in &mut stmt.items {
14532 if let SelectItem::Expr { expr, .. } = item {
14533 Self::substitute_grouping_calls(expr, &[]);
14534 }
14535 }
14536 if let Some(h) = &mut stmt.having {
14537 Self::substitute_grouping_calls(h, &[]);
14538 }
14539 }
14540 }
14541 if grouping_sets.len() > 1 {
14542 // The primary set's own dropped keys nullify in the
14543 // HEAD's projection too (GROUPING SETS's first set may
14544 // omit keys other sets use).
14545 let primary = grouping_sets[0].clone();
14546 let head_dropped: Vec<Expr> = grouping_universe
14547 .iter()
14548 .filter(|u| !primary.iter().any(|k| k == *u))
14549 .cloned()
14550 .collect();
14551 for set in grouping_sets.iter().skip(1) {
14552 let mut peer = stmt.clone();
14553 peer.unions = Vec::new();
14554 let dropped: Vec<&Expr> = grouping_universe
14555 .iter()
14556 .filter(|u| !set.iter().any(|k| k == *u))
14557 .collect();
14558 // Empty set = grand-total group: `Some(vec![])` forces
14559 // the aggregate path (one collapsed row) instead of a
14560 // per-row passthrough. See the primary-set note above.
14561 peer.group_by = Some(set.clone());
14562 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14563 for item in &mut peer.items {
14564 if let SelectItem::Expr { expr, alias } = item {
14565 if dropped.iter().any(|d| *d == expr) {
14566 // v7.39 — keep the dropped key's name on the
14567 // NULL literal so the UNION output column
14568 // (and any top-level ORDER BY on it) still
14569 // resolves.
14570 if alias.is_none()
14571 && let Expr::Column(c) = &expr
14572 {
14573 *alias = Some(c.name.clone());
14574 }
14575 *expr = Expr::Literal(Literal::Null);
14576 } else {
14577 Self::substitute_grouping_calls(expr, &dropped_owned);
14578 }
14579 }
14580 }
14581 if let Some(h) = &mut peer.having {
14582 Self::substitute_grouping_calls(h, &dropped_owned);
14583 }
14584 stmt.unions.push((UnionKind::All, peer));
14585 }
14586 for item in &mut stmt.items {
14587 if let SelectItem::Expr { expr, alias } = item {
14588 if head_dropped.iter().any(|d| d == expr) {
14589 if alias.is_none()
14590 && let Expr::Column(c) = &expr
14591 {
14592 *alias = Some(c.name.clone());
14593 }
14594 *expr = Expr::Literal(Literal::Null);
14595 } else {
14596 Self::substitute_grouping_calls(expr, &head_dropped);
14597 }
14598 }
14599 }
14600 if let Some(h) = &mut stmt.having {
14601 Self::substitute_grouping_calls(h, &head_dropped);
14602 }
14603 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14604 // (while `grouping_universe` / the per-branch sets are in scope). For
14605 // each grouping() call in it, inject a per-branch hidden column
14606 // `__grp_ord_K` carrying that branch's mask into the head + every
14607 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14608 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14609 // from the final output. A standalone grouping-set query has ORDER BY
14610 // (not an explicit set-op) next, so consuming it here is safe.
14611 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14612 // rollup carries the hierarchical order: sort by the grouping
14613 // keys with the rolled-up NULLs last, which is exactly the
14614 // interleaving both oracles emit. A client's own ORDER BY wins,
14615 // which is what MySQL does (MariaDB refuses to let one be
14616 // written at all).
14617 // The synthesised keys have to travel the SAME path a written
14618 // ORDER BY does: the block below is what turns a `grouping()`
14619 // call into the per-branch `__grp_ord_K` column the engine can
14620 // actually sort on. Bypassing it left a bare `grouping(text)`
14621 // for the evaluator to reject.
14622 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14623 self.parse_order_by_keys()?
14624 } else if mysql_rollup {
14625 Self::mysql_rollup_order(&grouping_universe)
14626 } else {
14627 Vec::new()
14628 };
14629 if !synthesised_or_parsed.is_empty() {
14630 let mut order_keys = synthesised_or_parsed;
14631 let mut grp_exprs: Vec<Expr> = Vec::new();
14632 for ob in &order_keys {
14633 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14634 }
14635 for (k, gexpr) in grp_exprs.iter().enumerate() {
14636 let colname = alloc::format!("__grp_ord_{k}");
14637 // Head branch (primary set) uses `head_dropped`.
14638 let mut he = gexpr.clone();
14639 Self::substitute_grouping_calls(&mut he, &head_dropped);
14640 stmt.items.push(SelectItem::Expr {
14641 expr: he,
14642 alias: Some(colname.clone()),
14643 });
14644 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14645 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14646 let set = &grouping_sets[i + 1];
14647 let dropped: Vec<Expr> = grouping_universe
14648 .iter()
14649 .filter(|u| !set.iter().any(|k| k == *u))
14650 .cloned()
14651 .collect();
14652 let mut pe = gexpr.clone();
14653 Self::substitute_grouping_calls(&mut pe, &dropped);
14654 peer.items.push(SelectItem::Expr {
14655 expr: pe,
14656 alias: Some(colname.clone()),
14657 });
14658 }
14659 }
14660 for ob in &mut order_keys {
14661 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14662 }
14663 // v7.40.0 — and a KEY the order sorts on that the query
14664 // did not project.
14665 //
14666 // A UNION's ORDER BY can only name output columns, so
14667 // the rollup order synthesised over `grouping_universe`
14668 // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14669 // ROLLUP` and the query answered `column "qty" does not
14670 // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14671 // by the key whether or not it is selected. The key
14672 // travels as a hidden column, exactly as the grouping
14673 // mask above does, and is stripped from the output by
14674 // the same rule.
14675 let mut key_exprs: Vec<Expr> = Vec::new();
14676 for ob in &order_keys {
14677 let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14678 let projected = stmt
14679 .items
14680 .iter()
14681 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14682 if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14683 key_exprs.push(ob.expr.clone());
14684 }
14685 }
14686 for (k, kexpr) in key_exprs.iter().enumerate() {
14687 let colname = alloc::format!("__grp_key_{k}");
14688 let mut he = kexpr.clone();
14689 Self::substitute_grouping_calls(&mut he, &head_dropped);
14690 stmt.items.push(SelectItem::Expr {
14691 expr: he,
14692 alias: Some(colname.clone()),
14693 });
14694 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14695 let set = &grouping_sets[i + 1];
14696 let dropped: Vec<Expr> = grouping_universe
14697 .iter()
14698 .filter(|u| !set.iter().any(|kk| kk == *u))
14699 .cloned()
14700 .collect();
14701 let mut pe = kexpr.clone();
14702 Self::substitute_grouping_calls(&mut pe, &dropped);
14703 peer.items.push(SelectItem::Expr {
14704 expr: pe,
14705 alias: Some(colname.clone()),
14706 });
14707 }
14708 for ob in &mut order_keys {
14709 if &ob.expr == kexpr {
14710 ob.expr = Expr::Column(crate::ast::ColumnName {
14711 name: colname.clone(),
14712 qualifier: None,
14713 });
14714 }
14715 }
14716 }
14717 stmt.order_by = order_keys;
14718 }
14719 }
14720 Ok(stmt)
14721 }
14722
14723 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14724 /// as ORDER BY keys.
14725 ///
14726 /// Per key: the rollup marker, then the key. Sorting on the key alone
14727 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14728 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14729 /// the ROLLUP-introduced NULL last, and both print as NULL.
14730 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14731 /// real group including the data-NULL one, 1 only for the row the
14732 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14733 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14734 ///
14735 /// `#[inline(never)]`: its locals must not join the statement parser's
14736 /// frame, which round 430 measured sitting against the nesting guard.
14737 #[inline(never)]
14738 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14739 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14740 for e in keys {
14741 out.push(OrderBy {
14742 expr: Expr::FunctionCall {
14743 name: "grouping".into(),
14744 args: alloc::vec![e.clone()],
14745 },
14746 desc: false,
14747 nulls_first: None,
14748 collation: None,
14749 });
14750 out.push(OrderBy {
14751 expr: e.clone(),
14752 desc: false,
14753 // MySQL orders NULL first on an ascending key.
14754 nulls_first: Some(true),
14755 collation: None,
14756 });
14757 }
14758 out
14759 }
14760
14761 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14762 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14763 #[inline(never)]
14764 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14765 use crate::ast::MaintainKind;
14766 self.skip_paren_option_list();
14767 let kind = match self.peek() {
14768 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14769 Token::Table | Token::Index => {
14770 self.advance();
14771 MaintainKind::ReindexRelation
14772 }
14773 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14774 "index" | "table" => {
14775 self.advance();
14776 MaintainKind::ReindexRelation
14777 }
14778 "schema" => {
14779 self.advance();
14780 MaintainKind::ReindexSchema
14781 }
14782 "system" | "database" => {
14783 self.advance();
14784 MaintainKind::Whole
14785 }
14786 // PG requires the object type; anything else is the
14787 // caller's problem, not something to swallow.
14788 _ => MaintainKind::ReindexRelation,
14789 },
14790 _ => MaintainKind::Whole,
14791 };
14792 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14793 // allows the plain form, so the modifier is recorded rather than
14794 // skipped. It still has no effect on how the reindex runs.
14795 let mut concurrently = false;
14796 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14797 self.advance();
14798 concurrently = true;
14799 }
14800 let target = self.take_optional_maintain_name();
14801 self.consume_until_statement_boundary();
14802 Ok(Statement::Maintain {
14803 kind,
14804 concurrently,
14805 target,
14806 })
14807 }
14808
14809 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14810 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14811 #[inline(never)]
14812 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14813 use crate::ast::MaintainKind;
14814 self.skip_paren_option_list();
14815 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14816 self.advance();
14817 }
14818 let target = self.take_optional_maintain_name();
14819 self.consume_until_statement_boundary();
14820 Ok(Statement::Maintain {
14821 kind: if target.is_some() {
14822 MaintainKind::ClusterRelation
14823 } else {
14824 MaintainKind::Whole
14825 },
14826 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14827 // transaction block quite happily (measured).
14828 concurrently: false,
14829 target,
14830 })
14831 }
14832
14833 /// The next token as a relation / schema name, when there is one.
14834 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14835 match self.peek() {
14836 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14837 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14838 _ => None,
14839 },
14840 _ => None,
14841 }
14842 }
14843
14844 /// A parenthesised option list, absorbed.
14845 fn skip_paren_option_list(&mut self) {
14846 if !matches!(self.peek(), Token::LParen) {
14847 return;
14848 }
14849 let mut depth = 0usize;
14850 loop {
14851 match self.advance() {
14852 Token::LParen => depth += 1,
14853 Token::RParen => {
14854 depth -= 1;
14855 if depth == 0 {
14856 return;
14857 }
14858 }
14859 Token::Eof => return,
14860 _ => {}
14861 }
14862 }
14863 }
14864
14865 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14866 /// column list.
14867 ///
14868 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14869 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14870 /// / ALL. The three that describe physical storage have no meaning
14871 /// here, so they parse and change nothing rather than making a
14872 /// dump that mentions them fail to load.
14873 ///
14874 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14875 /// parse chain the nesting sentinel is tuned against.
14876 #[inline(never)]
14877 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14878 self.advance(); // LIKE
14879 let source = self.expect_ident_like()?;
14880 let mut options = crate::ast::LikeOptions::default();
14881 loop {
14882 let including = match self.peek() {
14883 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14884 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14885 _ => break,
14886 };
14887 self.advance();
14888 // `ALL` lexes as its own keyword, not an identifier.
14889 let opt = if matches!(self.peek(), Token::All) {
14890 self.advance();
14891 alloc::string::String::from("all")
14892 } else {
14893 self.expect_ident_like()?
14894 };
14895 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14896 o.defaults = on;
14897 o.constraints = on;
14898 o.identity = on;
14899 o.generated = on;
14900 o.indexes = on;
14901 o.comments = on;
14902 };
14903 match opt.to_ascii_lowercase().as_str() {
14904 "all" => set(&mut options, including),
14905 "defaults" => options.defaults = including,
14906 "constraints" => options.constraints = including,
14907 "identity" => options.identity = including,
14908 "generated" => options.generated = including,
14909 "indexes" => options.indexes = including,
14910 "comments" => options.comments = including,
14911 // No storage model to copy into.
14912 "storage" | "statistics" | "compression" => {}
14913 other => {
14914 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14915 }
14916 }
14917 }
14918 Ok(crate::ast::LikeSpec {
14919 source,
14920 at,
14921 options,
14922 keep_index_names: false,
14923 })
14924 }
14925
14926 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14927 // Caller already consumed CREATE; we're sitting on TABLE.
14928 debug_assert!(matches!(self.peek(), Token::Table));
14929 self.advance();
14930 let if_not_exists = self.consume_if_not_exists();
14931 let name = self.expect_ident_like()?;
14932 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14933 // child shape has no column list; the child inherits its
14934 // columns from the parent at engine-DDL time. Detect it
14935 // before the `(` requirement below.
14936 if matches!(self.peek(), Token::Partition)
14937 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14938 {
14939 self.advance(); // PARTITION
14940 self.advance(); // of
14941 let partition_of = self.parse_partition_of_tail()?;
14942 return Ok(Statement::CreateTable(CreateTableStatement {
14943 temporary: false,
14944 name,
14945 engine: None,
14946 auto_increment: None,
14947 columns: Vec::new(),
14948 like_specs: Vec::new(),
14949 inherits: Vec::new(),
14950 if_not_exists,
14951 foreign_keys: Vec::new(),
14952 table_constraints: Vec::new(),
14953 partition_by: None,
14954 partition_of: Some(partition_of),
14955 }));
14956 }
14957 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14958 // the materialized-view materialisation path (run the SELECT, infer the
14959 // column types, create + populate the table) but marks the node so the
14960 // executor creates a plain table without a mat-view registry entry.
14961 if matches!(self.peek(), Token::As) {
14962 self.advance();
14963 let body_stmt = self.parse_select_stmt()?;
14964 let Statement::Select(body) = body_stmt else {
14965 return Err(self.err(format!(
14966 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14967 )));
14968 };
14969 let with_data = self.parse_optional_with_data(true)?;
14970 return Ok(Statement::CreateMaterializedView(
14971 crate::ast::CreateMaterializedViewStatement {
14972 temporary: false,
14973 name,
14974 if_not_exists,
14975 columns: Vec::new(),
14976 body,
14977 with_data,
14978 as_plain_table: true,
14979 },
14980 ));
14981 }
14982 // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14983 // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14984 // written without the parentheses. It was a syntax error, so a
14985 // schema written against MySQL could not be loaded at all.
14986 //
14987 // Measured on MySQL 9.7.2: the copy takes the columns, their
14988 // defaults and the indexes, and takes neither the rows nor the
14989 // foreign keys — which is exactly `INCLUDING ALL` here, since
14990 // SPG's LIKE has never copied foreign keys.
14991 if matches!(self.peek(), Token::Like) {
14992 let at = self.pos;
14993 let spec = self.parse_create_table_like(at)?;
14994 let spec = crate::ast::LikeSpec {
14995 options: crate::ast::LikeOptions {
14996 defaults: true,
14997 constraints: true,
14998 identity: true,
14999 generated: true,
15000 indexes: true,
15001 comments: true,
15002 },
15003 keep_index_names: true,
15004 ..spec
15005 };
15006 return Ok(Statement::CreateTable(CreateTableStatement {
15007 temporary: false,
15008 name,
15009 engine: None,
15010 auto_increment: None,
15011 columns: Vec::new(),
15012 like_specs: alloc::vec![spec],
15013 inherits: Vec::new(),
15014 if_not_exists,
15015 foreign_keys: Vec::new(),
15016 table_constraints: Vec::new(),
15017 partition_by: None,
15018 partition_of: None,
15019 }));
15020 }
15021 if !matches!(self.peek(), Token::LParen) {
15022 return Err(self.err(format!(
15023 "expected '(' after table name, got {:?}",
15024 self.peek()
15025 )));
15026 }
15027 self.advance();
15028 let mut columns = Vec::new();
15029 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
15030 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
15031 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
15032 loop {
15033 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
15034 // column list. It is how a child that adds nothing of its own is
15035 // written, and this loop demanded at least one entry: `syntax
15036 // error at or near ")"`. The child takes the parent's columns,
15037 // which the INHERITS clause already arranges.
15038 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
15039 self.advance();
15040 break;
15041 }
15042 // v7.6.0 / v7.9.18 — distinguish table-level constraint
15043 // clauses from column definitions. Constraints start
15044 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
15045 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
15046 // a column.
15047 if self.peek_table_level_pk_start() {
15048 table_constraints.push(self.parse_table_level_primary_key()?);
15049 } else if matches!(self.peek(), Token::Like) {
15050 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
15051 // <opt> ]*`. The source table's shape lives in the catalog,
15052 // so this records the clause and the engine expands it.
15053 like_specs.push(self.parse_create_table_like(columns.len())?);
15054 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
15055 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
15056 table_constraints.push(self.parse_table_level_exclude()?);
15057 } else if self.peek_table_level_unique_start() {
15058 table_constraints.push(self.parse_table_level_unique()?);
15059 } else if self.peek_table_level_check_start() {
15060 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
15061 table_constraints.push(self.parse_table_level_check()?);
15062 } else if self.peek_mysql_inline_key_start() {
15063 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
15064 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
15065 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
15066 // inside the column list. Skip name + paren list;
15067 // for UNIQUE KEY, register as a UC.
15068 if let Some(uc) = self.parse_mysql_inline_key()? {
15069 table_constraints.push(uc);
15070 }
15071 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
15072 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
15073 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
15074 // CHECK is named, and the named-CONSTRAINT arm used
15075 // to accept FOREIGN KEY only. The name is accepted
15076 // and discarded — same handling as every other SPG
15077 // constraint name.
15078 self.advance(); // CONSTRAINT
15079 // v7.39 (read01 round 48) — the name is kept now: the schema
15080 // stores it, so DROP / RENAME CONSTRAINT can find it.
15081 let con_name = self.expect_ident_like()?;
15082 let mut tc = match kind {
15083 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
15084 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
15085 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
15086 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
15087 };
15088 match &mut tc {
15089 crate::ast::TableConstraint::Check { name, .. }
15090 | crate::ast::TableConstraint::Unique { name, .. }
15091 | crate::ast::TableConstraint::PrimaryKey { name, .. }
15092 | crate::ast::TableConstraint::Exclude { name, .. } => {
15093 *name = Some(con_name);
15094 }
15095 _ => {}
15096 }
15097 table_constraints.push(tc);
15098 } else if self.peek_constraint_or_fk_start() {
15099 foreign_keys.push(self.parse_table_level_fk()?);
15100 } else {
15101 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15102 // v7.13.0 — fold inline UNIQUE / CHECK column
15103 // constraints into table-level entries so the
15104 // engine path stays uniform.
15105 if col.is_unique {
15106 table_constraints.push(crate::ast::TableConstraint::Unique {
15107 name: None,
15108 columns: alloc::vec![col.name.clone()],
15109 nulls_not_distinct: col.unique_nulls_not_distinct,
15110 deferrable: col.constraint_deferrable,
15111 initially_deferred: col.constraint_initially_deferred,
15112 prefix_lengths: Vec::new(),
15113 });
15114 }
15115 if let Some(check_expr) = col.check.clone() {
15116 table_constraints.push(crate::ast::TableConstraint::Check {
15117 name: None,
15118 expr: check_expr,
15119 not_valid: false,
15120 });
15121 }
15122 columns.push(col);
15123 if let Some(fk) = col_level_fk {
15124 foreign_keys.push(fk);
15125 }
15126 }
15127 match self.peek() {
15128 Token::Comma => {
15129 self.advance();
15130 }
15131 Token::RParen => {
15132 self.advance();
15133 break;
15134 }
15135 other => {
15136 return Err(
15137 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15138 );
15139 }
15140 }
15141 }
15142 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15143 // `CREATE TABLE k (LIKE t)` is a complete definition even though
15144 // nothing is written between the parentheses.
15145 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15146 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15147 // empty parentheses were a parse error in their own right — quite apart
15148 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15149 // SPG does not have (filed separately).
15150 let _ = &like_specs;
15151 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15152 // It sits between the column list and the MySQL table options,
15153 // and it was a syntax error until this round.
15154 let mut inherits: Vec<String> = Vec::new();
15155 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15156 if k.eq_ignore_ascii_case("inherits"))
15157 {
15158 self.advance();
15159 if !matches!(self.peek(), Token::LParen) {
15160 return Err(self.err(alloc::format!(
15161 "expected ( after INHERITS, got {:?}",
15162 self.peek()
15163 )));
15164 }
15165 self.advance();
15166 loop {
15167 inherits.push(self.expect_ident_like()?);
15168 if matches!(self.peek(), Token::Comma) {
15169 self.advance();
15170 continue;
15171 }
15172 break;
15173 }
15174 if !matches!(self.peek(), Token::RParen) {
15175 return Err(self.err(alloc::format!(
15176 "expected ) closing INHERITS, got {:?}",
15177 self.peek()
15178 )));
15179 }
15180 self.advance();
15181 }
15182 // v7.14.0 — consume MySQL/MariaDB table options after the
15183 // closing `)`. mysqldump emits things like
15184 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15185 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15186 // SPG accepts all forms as no-ops (each option is
15187 // `<ident> [=] <ident-or-string>` separated by whitespace).
15188 let (engine, auto_increment) = self.consume_mysql_table_options();
15189 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15190 // SPG has no per-table reloptions, so accept and ignore them so a
15191 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15192 self.consume_with_reloptions();
15193 // v7.37.6-B — declarative-partition-parent suffix
15194 // (`PARTITION BY RANGE (key_col)`) sits after the column
15195 // list + MySQL table-options. v7.37.6-B only accepts RANGE
15196 // and locks the key column at one ident; the engine then
15197 // verifies the column type is TIMESTAMPTZ.
15198 let partition_by = if matches!(self.peek(), Token::Partition) {
15199 self.advance(); // PARTITION
15200 if !self.peek_is_by() {
15201 return Err(self.err(format!(
15202 "expected BY after PARTITION, got {:?}",
15203 self.peek()
15204 )));
15205 }
15206 self.advance();
15207 Some(self.parse_partition_by_tail()?)
15208 } else {
15209 None
15210 };
15211 Ok(Statement::CreateTable(CreateTableStatement {
15212 temporary: false,
15213 name,
15214 engine,
15215 auto_increment,
15216 columns,
15217 like_specs,
15218 inherits,
15219 if_not_exists,
15220 foreign_keys,
15221 table_constraints,
15222 partition_by,
15223 partition_of: None,
15224 }))
15225 }
15226
15227 /// v7.37.6-B — case-insensitive ident match helper for the
15228 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15229 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15230 /// didn't burn a global keyword slot for each (see the
15231 /// `Token::Partition` doc-comment in `lexer.rs`).
15232 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15233 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15234 }
15235
15236 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15237 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15238 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15239 use crate::ast::{PartitionBySpec, PartitionKindAst};
15240 let kind = match self.peek() {
15241 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15242 self.advance();
15243 PartitionKindAst::Range
15244 }
15245 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15246 self.advance();
15247 PartitionKindAst::List
15248 }
15249 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15250 self.advance();
15251 PartitionKindAst::Hash
15252 }
15253 other => {
15254 return Err(self.err(format!(
15255 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15256 )));
15257 }
15258 };
15259 if !matches!(self.peek(), Token::LParen) {
15260 return Err(self.err(format!(
15261 "expected '(' after PARTITION BY <strategy>, got {:?}",
15262 self.peek()
15263 )));
15264 }
15265 self.advance();
15266 let mut key_columns = Vec::new();
15267 loop {
15268 key_columns.push(self.expect_ident_like()?);
15269 match self.peek() {
15270 Token::Comma => {
15271 self.advance();
15272 }
15273 Token::RParen => {
15274 self.advance();
15275 break;
15276 }
15277 other => {
15278 return Err(self.err(format!(
15279 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15280 )));
15281 }
15282 }
15283 }
15284 if key_columns.is_empty() {
15285 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15286 }
15287 Ok(PartitionBySpec { kind, key_columns })
15288 }
15289
15290 /// v7.37.6-B — after `PARTITION OF`, expect
15291 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15292 /// or
15293 /// <parent> DEFAULT
15294 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15295 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15296 let parent_name = self.expect_ident_like()?;
15297 // v7.37.6-B rejects an explicit column list — the child
15298 // inherits from the parent. mailrs round-7 taught us that
15299 // CREATE TABLE-side schema reconciliation hides drift, so
15300 // we surface this as a parse error rather than silently
15301 // ignoring user columns.
15302 if matches!(self.peek(), Token::LParen) {
15303 return Err(self.err(
15304 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15305 at v7.37.6-B; the child inherits its columns from the parent"
15306 .to_string(),
15307 ));
15308 }
15309 let bounds = match self.peek() {
15310 Token::Default => {
15311 self.advance();
15312 PartitionOfBoundsAst::Default
15313 }
15314 Token::For => {
15315 self.advance();
15316 if !matches!(self.peek(), Token::Values) {
15317 return Err(
15318 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15319 );
15320 }
15321 self.advance();
15322 // WITH is not a reserved Token in the lexer — it lexes
15323 // as Token::Ident("with"). Disambiguate manually.
15324 let want_with = matches!(
15325 self.peek(),
15326 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15327 );
15328 if want_with {
15329 self.advance();
15330 if !matches!(self.peek(), Token::LParen) {
15331 return Err(self.err(format!(
15332 "expected '(' after FOR VALUES WITH, got {:?}",
15333 self.peek()
15334 )));
15335 }
15336 self.advance();
15337 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15338 loop {
15339 let key = self.expect_ident_like()?;
15340 let n = match self.peek().clone() {
15341 Token::Integer(v) if u32::try_from(v).is_ok() => {
15342 self.advance();
15343 v as u32
15344 }
15345 other => {
15346 return Err(self.err(format!(
15347 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15348 )));
15349 }
15350 };
15351 match key.to_ascii_uppercase().as_str() {
15352 "MODULUS" => modulus = Some(n),
15353 "REMAINDER" => remainder = Some(n),
15354 other => {
15355 return Err(self.err(format!(
15356 "FOR VALUES WITH: unknown key {other:?}; \
15357 expected MODULUS or REMAINDER"
15358 )));
15359 }
15360 }
15361 match self.peek() {
15362 Token::Comma => {
15363 self.advance();
15364 }
15365 Token::RParen => {
15366 self.advance();
15367 break;
15368 }
15369 other => {
15370 return Err(self.err(format!(
15371 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15372 )));
15373 }
15374 }
15375 }
15376 let modulus = modulus
15377 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15378 let remainder = remainder.ok_or_else(|| {
15379 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15380 })?;
15381 if modulus == 0 {
15382 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15383 }
15384 if remainder >= modulus {
15385 return Err(self.err(format!(
15386 "FOR VALUES WITH: REMAINDER ({remainder}) \
15387 must be < MODULUS ({modulus})"
15388 )));
15389 }
15390 PartitionOfBoundsAst::Hash { modulus, remainder }
15391 } else {
15392 match self.peek() {
15393 Token::From => {
15394 self.advance();
15395 let lower = Box::new(self.parse_partition_bound_expr()?);
15396 if !matches!(self.peek(), Token::To) {
15397 return Err(self.err(format!(
15398 "expected TO after FROM (...), got {:?}",
15399 self.peek()
15400 )));
15401 }
15402 self.advance();
15403 let upper = Box::new(self.parse_partition_bound_expr()?);
15404 PartitionOfBoundsAst::Range { lower, upper }
15405 }
15406 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15407 Token::In => {
15408 self.advance();
15409 if !matches!(self.peek(), Token::LParen) {
15410 return Err(self.err(format!(
15411 "expected '(' after FOR VALUES IN, got {:?}",
15412 self.peek()
15413 )));
15414 }
15415 self.advance();
15416 let mut values = Vec::new();
15417 loop {
15418 values.push(self.parse_expr(0)?);
15419 match self.peek() {
15420 Token::Comma => {
15421 self.advance();
15422 }
15423 Token::RParen => {
15424 self.advance();
15425 break;
15426 }
15427 other => {
15428 return Err(self.err(format!(
15429 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15430 )));
15431 }
15432 }
15433 }
15434 if values.is_empty() {
15435 return Err(self.err(
15436 "FOR VALUES IN requires at least one literal".to_string(),
15437 ));
15438 }
15439 PartitionOfBoundsAst::List { values }
15440 }
15441 other => {
15442 return Err(self.err(format!(
15443 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15444 )));
15445 }
15446 }
15447 }
15448 }
15449 other => {
15450 return Err(self.err(format!(
15451 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15452 )));
15453 }
15454 };
15455 Ok(PartitionOfSpec {
15456 parent_name,
15457 bounds,
15458 })
15459 }
15460
15461 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15462 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15463 /// markers (no-arg builtins) so the engine resolves them
15464 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15465 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15466 if !matches!(self.peek(), Token::LParen) {
15467 return Err(self.err(format!(
15468 "expected '(' before partition bound, got {:?}",
15469 self.peek()
15470 )));
15471 }
15472 self.advance();
15473 let expr = match self.peek() {
15474 Token::Ident(s) | Token::QuotedIdent(s)
15475 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15476 {
15477 let name = s.to_ascii_uppercase();
15478 self.advance();
15479 crate::ast::Expr::FunctionCall {
15480 name,
15481 args: Vec::new(),
15482 }
15483 }
15484 _ => self.parse_expr(0)?,
15485 };
15486 if !matches!(self.peek(), Token::RParen) {
15487 return Err(self.err(format!(
15488 "expected ')' after partition bound, got {:?}",
15489 self.peek()
15490 )));
15491 }
15492 self.advance();
15493 Ok(expr)
15494 }
15495
15496 /// v7.14.0 — true when the next tokens look like an inline
15497 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15498 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15499 /// — each followed by an optional name + `(...)`. Critical:
15500 /// a column NAMED `key` / `index` (PG accepts as ident) must
15501 /// NOT be mistaken for the KEY constraint shape. We disambig
15502 /// by requiring the keyword to be followed by either `(` or
15503 /// `<ident> (`.
15504 fn peek_mysql_inline_key_start(&self) -> bool {
15505 let cur = self.peek();
15506 // Shapes:
15507 // KEY (cols)
15508 // KEY name (cols)
15509 // INDEX (cols)
15510 // INDEX name (cols)
15511 // UNIQUE KEY [name] (cols)
15512 // UNIQUE INDEX [name] (cols)
15513 // FULLTEXT [KEY|INDEX] [name] (cols)
15514 // SPATIAL [KEY|INDEX] [name] (cols)
15515 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15516 // tokens at skip = the position AFTER the index-form
15517 // keywords (KEY/INDEX) have been consumed.
15518 match self.tokens.get(skip) {
15519 Some(Token::LParen) => true,
15520 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15521 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15522 }
15523 _ => false,
15524 }
15525 };
15526 // `INDEX` lexes as Token::Index (reserved), not as
15527 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15528 // start; the peek helper below handles either.
15529 let is_key_or_index_tok = |t: &Token| -> bool {
15530 matches!(t, Token::Index)
15531 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15532 };
15533 match cur {
15534 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15535 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15536 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15537 }
15538 Token::Ident(s)
15539 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15540 {
15541 let nxt = self.tokens.get(self.pos + 1);
15542 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15543 self.pos + 2
15544 } else {
15545 self.pos + 1
15546 };
15547 after_keyword_followed_by_paren_or_ident_paren(after_after)
15548 }
15549 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15550 let nxt = self.tokens.get(self.pos + 1);
15551 if !nxt.is_some_and(is_key_or_index_tok) {
15552 return false;
15553 }
15554 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15555 }
15556 _ => false,
15557 }
15558 }
15559
15560 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15561 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15562 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15563 /// returns Some(TableConstraint::Index) so the engine builds
15564 /// a real BTree index on the leading column (mysqldump
15565 /// `KEY idx_posts_author (author_id)` shape).
15566 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15567 /// (the storage layer has no matching AM).
15568 fn parse_mysql_inline_key(
15569 &mut self,
15570 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15571 // Detect UNIQUE prefix.
15572 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15573 {
15574 self.advance();
15575 true
15576 } else {
15577 false
15578 };
15579 // Consume FULLTEXT / SPATIAL prefix and record which one
15580 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15581 // dedicated TableConstraint variant so the engine can
15582 // build a tsvector-GIN; SPATIAL still has no matching
15583 // AM, so it falls back to accept-as-no-op.
15584 let mut is_fulltext = false;
15585 let mut is_spatial = false;
15586 if let Token::Ident(s) = self.peek().clone() {
15587 if s.eq_ignore_ascii_case("fulltext") {
15588 self.advance();
15589 is_fulltext = true;
15590 } else if s.eq_ignore_ascii_case("spatial") {
15591 self.advance();
15592 is_spatial = true;
15593 }
15594 }
15595 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15596 // (reserved); accept either token shape.
15597 match self.peek() {
15598 Token::Index => {
15599 self.advance();
15600 }
15601 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15602 self.advance();
15603 }
15604 other => {
15605 return Err(self.err(alloc::format!(
15606 "expected KEY/INDEX in inline index declaration, got {other:?}"
15607 )));
15608 }
15609 }
15610 // Optional index name (an ident before the `(`).
15611 // v7.15.0 — capture the name when present so the engine
15612 // builds the secondary index under the user's chosen
15613 // name (matches mysqldump's `KEY idx_x (col)` shape).
15614 let mut idx_name: Option<String> = None;
15615 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15616 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15617 {
15618 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15619 idx_name = Some(s);
15620 }
15621 }
15622 // Optional `USING BTREE` / `USING HASH` (MySQL).
15623 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15624 self.advance();
15625 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15626 self.advance();
15627 }
15628 }
15629 // Required column list `(col [, col]*)`.
15630 if !matches!(self.peek(), Token::LParen) {
15631 return Err(self.err(alloc::format!(
15632 "expected '(' in inline KEY/INDEX, got {:?}",
15633 self.peek()
15634 )));
15635 }
15636 self.advance();
15637 let mut cols: Vec<String> = Vec::new();
15638 let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15639 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15640 self.advance();
15641 cols.push(s);
15642 // v7.40.0 — the per-column `(length)` prefix is KEPT.
15643 //
15644 // It used to be skipped, so `KEY kb (b(4))` was accepted and
15645 // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15646 // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15647 // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15648 // then unrecorded is the worst of the three answers.
15649 let mut prefix: Option<u32> = None;
15650 if matches!(self.peek(), Token::LParen) {
15651 let mut depth = 1usize;
15652 self.advance();
15653 if let Token::Integer(n) = self.peek()
15654 && let Ok(v) = u32::try_from(*n)
15655 {
15656 prefix = Some(v);
15657 }
15658 while depth > 0 {
15659 match self.peek() {
15660 Token::LParen => depth += 1,
15661 Token::RParen => depth -= 1,
15662 Token::Eof => break,
15663 _ => {}
15664 }
15665 self.advance();
15666 }
15667 }
15668 prefix_lengths.push(prefix);
15669 // Skip optional ASC / DESC.
15670 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15671 || matches!(self.peek(), Token::Asc | Token::Desc)
15672 {
15673 self.advance();
15674 }
15675 if matches!(self.peek(), Token::Comma) {
15676 self.advance();
15677 continue;
15678 }
15679 break;
15680 }
15681 if matches!(self.peek(), Token::RParen) {
15682 self.advance();
15683 }
15684 // Trailing options on the inline index — comment / etc.
15685 // Skip until comma or `)`.
15686 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15687 self.advance();
15688 }
15689 if cols.is_empty() {
15690 return Ok(None);
15691 }
15692 if is_unique {
15693 // Carry the captured idx_name on UNIQUE too so future
15694 // engine work can name the underlying BTree
15695 // accordingly; today the unique-constraint installer
15696 // synthesises the name itself, but Display round-trip
15697 // benefits from preserving it.
15698 Ok(Some(crate::ast::TableConstraint::Unique {
15699 name: idx_name,
15700 columns: cols,
15701 nulls_not_distinct: false,
15702 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15703 deferrable: false,
15704 initially_deferred: false,
15705 prefix_lengths,
15706 }))
15707 } else if is_fulltext {
15708 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15709 // routes through `TableConstraint::FulltextIndex`;
15710 // the engine builds a tsvector-GIN over each named
15711 // column so MATCH AGAINST gets a real inverted
15712 // index instead of a silently-dropped declaration.
15713 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15714 name: idx_name,
15715 columns: cols,
15716 }))
15717 } else if is_spatial {
15718 // SPG has no native SPATIAL AM. Accept-as-no-op
15719 // (declaration is parsed, but no index is built).
15720 Ok(None)
15721 } else {
15722 // v7.15.0 — plain KEY / INDEX builds a real BTree
15723 // secondary index.
15724 Ok(Some(crate::ast::TableConstraint::Index {
15725 name: idx_name,
15726 columns: cols,
15727 prefix_lengths,
15728 }))
15729 }
15730 }
15731
15732 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15733 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15734 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15735 /// (in any order, separated by whitespace).
15736 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15737 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15738 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15739 /// bare ident here, and only the parenthesised form is reloptions (so this
15740 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15741 fn consume_with_reloptions(&mut self) {
15742 let is_with = matches!(
15743 self.peek(),
15744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15745 );
15746 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15747 return;
15748 }
15749 self.advance(); // WITH
15750 self.advance(); // (
15751 let mut depth = 1u32;
15752 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15753 match self.peek() {
15754 Token::LParen => depth += 1,
15755 Token::RParen => depth -= 1,
15756 _ => {}
15757 }
15758 self.advance();
15759 }
15760 }
15761
15762 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15763 /// dropped with everything else here. The rest of the MySQL table
15764 /// options genuinely have no meaning for SPG's storage; the engine
15765 /// name does, because MySQL REFUSES one it does not know and a dump
15766 /// with a typo in it should not quietly become a table.
15767 fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15768 let mut engine: Option<alloc::string::String> = None;
15769 let mut auto_increment: Option<i64> = None;
15770 loop {
15771 // Heuristic: a table option is an ident (or `DEFAULT`
15772 // reserved keyword) followed by `=` and an
15773 // ident / string / integer.
15774 let name_lc = match self.peek().clone() {
15775 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15776 Token::Default => alloc::string::String::from("default"),
15777 _ => break,
15778 };
15779 let known = matches!(
15780 name_lc.as_str(),
15781 "engine"
15782 | "default"
15783 | "charset"
15784 | "collate"
15785 | "auto_increment"
15786 | "row_format"
15787 | "comment"
15788 | "pack_keys"
15789 | "stats_persistent"
15790 | "stats_auto_recalc"
15791 | "stats_sample_pages"
15792 | "key_block_size"
15793 | "tablespace"
15794 | "min_rows"
15795 | "max_rows"
15796 | "checksum"
15797 | "delay_key_write"
15798 | "insert_method"
15799 | "data"
15800 | "index"
15801 | "encryption"
15802 | "compression"
15803 );
15804 if !known {
15805 break;
15806 }
15807 self.advance(); // option name
15808 // `DEFAULT` optional prefix is followed by `CHARSET` /
15809 // `COLLATE`; consume the next ident too.
15810 if name_lc == "default" {
15811 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15812 self.advance();
15813 }
15814 }
15815 if matches!(self.peek(), Token::Eq) {
15816 self.advance();
15817 }
15818 match self.peek().clone() {
15819 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15820 if name_lc == "engine" {
15821 // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15822 // engine it does not know and names it back
15823 // exactly: `Unknown storage engine 'NoSuchEng'`,
15824 // measured. The lexer folds a bare identifier, so
15825 // the message quoted a name the dump did not
15826 // contain, which is the one thing that message is
15827 // for. Guarded the same way the column spelling
15828 // is: the span runs to the next token, so what
15829 // comes back has to be the same word.
15830 let written = self
15831 .source_span(self.pos, self.pos)
15832 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15833 .filter(|raw| raw.eq_ignore_ascii_case(&v))
15834 .map(alloc::string::String::from);
15835 engine = Some(written.unwrap_or(v));
15836 }
15837 self.advance();
15838 }
15839 Token::Integer(v) => {
15840 // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15841 // the table hands out, and it was consumed and
15842 // dropped: measured on MySQL 9.7.2, the first row
15843 // inserted into a table declared that way gets 100,
15844 // where SPG gave it 1. `SHOW CREATE TABLE` already
15845 // reproduces the option from the counter, so the
15846 // dump round-tripped through a different number.
15847 if name_lc == "auto_increment" {
15848 auto_increment = Some(v);
15849 }
15850 self.advance();
15851 }
15852 _ => {}
15853 }
15854 }
15855 (engine, auto_increment)
15856 }
15857
15858 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15859 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15860 /// sure (otherwise a column literally named `primary` would
15861 /// be mistaken).
15862 fn peek_table_level_pk_start(&self) -> bool {
15863 let cur = self.peek();
15864 let nxt = self.tokens.get(self.pos + 1);
15865 let nxt2 = self.tokens.get(self.pos + 2);
15866 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15867 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15868 let is_lparen = matches!(nxt2, Some(Token::LParen));
15869 is_primary && is_key && is_lparen
15870 }
15871
15872 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15873 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15874 /// (mailrs round-5 G10).
15875 fn peek_table_level_unique_start(&self) -> bool {
15876 let cur = self.peek();
15877 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15878 if !is_unique {
15879 return false;
15880 }
15881 let n1 = self.tokens.get(self.pos + 1);
15882 // Plain `UNIQUE (…)`.
15883 if matches!(n1, Some(Token::LParen)) {
15884 return true;
15885 }
15886 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15887 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15888 if !is_nulls {
15889 return false;
15890 }
15891 let n2 = self.tokens.get(self.pos + 2);
15892 let n3 = self.tokens.get(self.pos + 3);
15893 let n4 = self.tokens.get(self.pos + 4);
15894 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15895 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15896 return true;
15897 }
15898 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15899 if matches!(n2, Some(Token::Not))
15900 && matches!(n3, Some(Token::Distinct))
15901 && matches!(n4, Some(Token::LParen))
15902 {
15903 return true;
15904 }
15905 false
15906 }
15907
15908 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15909 self.advance(); // PRIMARY
15910 self.advance(); // KEY
15911 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15912 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15913 // 621 consumed and dropped them (the storing half of F08).
15914 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15915 Ok(crate::ast::TableConstraint::PrimaryKey {
15916 name: None,
15917 columns,
15918 deferrable,
15919 initially_deferred,
15920 })
15921 }
15922
15923 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15924 self.advance(); // UNIQUE
15925 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15926 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15927 // is `NULLS DISTINCT` per the SQL standard.
15928 let mut nulls_not_distinct = false;
15929 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15930 let n1 = self.tokens.get(self.pos + 1);
15931 let n2 = self.tokens.get(self.pos + 2);
15932 let is_not = matches!(n1, Some(Token::Not));
15933 let is_distinct = matches!(n2, Some(Token::Distinct));
15934 if is_not && is_distinct {
15935 self.advance(); // NULLS
15936 self.advance(); // NOT
15937 self.advance(); // DISTINCT
15938 nulls_not_distinct = true;
15939 } else if matches!(n1, Some(Token::Distinct)) {
15940 self.advance(); // NULLS
15941 self.advance(); // DISTINCT
15942 }
15943 }
15944 let columns = self.parse_paren_ident_list("UNIQUE")?;
15945 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15946 Ok(crate::ast::TableConstraint::Unique {
15947 name: None,
15948 columns,
15949 nulls_not_distinct,
15950 deferrable,
15951 initially_deferred,
15952 prefix_lengths: Vec::new(),
15953 })
15954 }
15955
15956 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15957 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15958 /// expression.
15959 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15960 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15961 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15962 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15963 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15964 /// commit: `NOT` starts no other suffix here, but reading both
15965 /// tokens before advancing keeps the caller's error message intact
15966 /// if someone writes `NOT NULL` by mistake.
15967 fn parse_not_valid_suffix(&mut self) -> bool {
15968 if !matches!(self.peek(), Token::Not) {
15969 return false;
15970 }
15971 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15972 {
15973 return false;
15974 }
15975 self.advance();
15976 self.advance();
15977 true
15978 }
15979
15980 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15981 self.advance(); // EXCLUDE
15982 // Optional `USING <method>`.
15983 let mut method = None;
15984 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15985 self.advance();
15986 method = Some(match self.advance() {
15987 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15988 other => {
15989 return Err(self.err(alloc::format!(
15990 "expected index method after USING, got {other:?}"
15991 )));
15992 }
15993 });
15994 }
15995 if !matches!(self.peek(), Token::LParen) {
15996 return Err(self.err(alloc::format!(
15997 "expected '(' after EXCLUDE, got {:?}",
15998 self.peek()
15999 )));
16000 }
16001 self.advance();
16002 let mut elements: Vec<(String, String)> = Vec::new();
16003 loop {
16004 let col = match self.advance() {
16005 Token::Ident(s) | Token::QuotedIdent(s) => s,
16006 other => {
16007 return Err(self.err(alloc::format!(
16008 "expected column name in EXCLUDE, got {other:?}"
16009 )));
16010 }
16011 };
16012 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16013 return Err(self.err(alloc::format!(
16014 "expected WITH after EXCLUDE column, got {:?}",
16015 self.peek()
16016 )));
16017 }
16018 self.advance();
16019 let op = match self.advance() {
16020 Token::InetOverlap => String::from("&&"),
16021 Token::Intersects => String::from("?#"),
16022 Token::IsBelow => String::from("<^"),
16023 Token::IsAbove => String::from(">^"),
16024 Token::PatternLt => String::from("~<~"),
16025 Token::PatternLtEq => String::from("~<=~"),
16026 Token::PatternGt => String::from("~>~"),
16027 Token::PatternGtEq => String::from("~>=~"),
16028 Token::TsMatchOld => String::from("@@@"),
16029 Token::Eq => String::from("="),
16030 Token::JsonContains => String::from("@>"),
16031 Token::JsonContainedBy => String::from("<@"),
16032 Token::OverLeft => String::from("&<"),
16033 Token::OverRight => String::from("&>"),
16034 other => {
16035 return Err(self.err(alloc::format!(
16036 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
16037 )));
16038 }
16039 };
16040 elements.push((col, op));
16041 if matches!(self.peek(), Token::Comma) {
16042 self.advance();
16043 continue;
16044 }
16045 break;
16046 }
16047 if !matches!(self.peek(), Token::RParen) {
16048 return Err(self.err(alloc::format!(
16049 "expected ')' to close EXCLUDE, got {:?}",
16050 self.peek()
16051 )));
16052 }
16053 self.advance();
16054 Ok(crate::ast::TableConstraint::Exclude {
16055 name: None,
16056 method,
16057 elements,
16058 })
16059 }
16060
16061 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
16062 self.advance(); // CHECK
16063 if !matches!(self.peek(), Token::LParen) {
16064 return Err(self.err(alloc::format!(
16065 "expected '(' after CHECK, got {:?}",
16066 self.peek()
16067 )));
16068 }
16069 self.advance();
16070 let expr = self.parse_expr(0)?;
16071 if !matches!(self.peek(), Token::RParen) {
16072 return Err(self.err(alloc::format!(
16073 "expected ')' to close CHECK predicate, got {:?}",
16074 self.peek()
16075 )));
16076 }
16077 self.advance();
16078 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
16079 // are no existing rows for PG to skip, so it rejects the suffix.
16080 Ok(crate::ast::TableConstraint::Check {
16081 name: None,
16082 expr,
16083 not_valid: false,
16084 })
16085 }
16086
16087 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
16088 fn peek_table_level_check_start(&self) -> bool {
16089 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
16090 }
16091
16092 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
16093 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
16094 /// on the dedicated FK path (`parse_table_level_fk` consumes its
16095 /// own CONSTRAINT prefix).
16096 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16097 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16098 return None;
16099 }
16100 // tokens[pos+1] is the constraint name (any ident-like);
16101 // tokens[pos+2] is the kind keyword.
16102 match self.tokens.get(self.pos + 2) {
16103 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16104 Some(NamedTableConstraintKind::Check)
16105 }
16106 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16107 Some(NamedTableConstraintKind::Unique)
16108 }
16109 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16110 Some(NamedTableConstraintKind::PrimaryKey)
16111 }
16112 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16113 Some(NamedTableConstraintKind::Exclude)
16114 }
16115 _ => None,
16116 }
16117 }
16118
16119 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16120 if !matches!(self.peek(), Token::LParen) {
16121 return Err(self.err(alloc::format!(
16122 "expected '(' after {ctx}, got {:?}",
16123 self.peek()
16124 )));
16125 }
16126 self.advance();
16127 let mut out = Vec::new();
16128 loop {
16129 out.push(self.expect_ident_like()?);
16130 match self.peek() {
16131 Token::Comma => {
16132 self.advance();
16133 }
16134 Token::RParen => {
16135 self.advance();
16136 break;
16137 }
16138 other => {
16139 return Err(self.err(alloc::format!(
16140 "expected ',' or ')' in {ctx} list, got {other:?}"
16141 )));
16142 }
16143 }
16144 }
16145 if out.is_empty() {
16146 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16147 }
16148 Ok(out)
16149 }
16150
16151 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16152 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16153 /// table-level FK; a column def never starts with either keyword
16154 /// (column names are not in this reserved set).
16155 fn peek_constraint_or_fk_start(&self) -> bool {
16156 let is_constraint_kw = matches!(
16157 self.peek(),
16158 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16159 );
16160 let is_foreign_kw = matches!(
16161 self.peek(),
16162 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16163 );
16164 is_constraint_kw || is_foreign_kw
16165 }
16166
16167 /// v7.6.0 — parse a table-level FK clause:
16168 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16169 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16170 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16171 let mut name: Option<String> = None;
16172 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16173 self.advance();
16174 name = Some(self.expect_ident_like()?);
16175 }
16176 // `FOREIGN`
16177 match self.advance() {
16178 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16179 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16180 }
16181 // `KEY`
16182 match self.advance() {
16183 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16184 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16185 }
16186 // `(col, col, ...)`
16187 if !matches!(self.peek(), Token::LParen) {
16188 return Err(self.err(format!(
16189 "expected '(' after FOREIGN KEY, got {:?}",
16190 self.peek()
16191 )));
16192 }
16193 self.advance();
16194 let mut columns = Vec::new();
16195 loop {
16196 columns.push(self.expect_ident_like()?);
16197 match self.peek() {
16198 Token::Comma => {
16199 self.advance();
16200 }
16201 Token::RParen => {
16202 self.advance();
16203 break;
16204 }
16205 other => {
16206 return Err(self.err(format!(
16207 "expected ',' or ')' in FK column list, got {other:?}"
16208 )));
16209 }
16210 }
16211 }
16212 if columns.is_empty() {
16213 return Err(self.err("FOREIGN KEY requires at least one column".into()));
16214 }
16215 let (
16216 parent_table,
16217 parent_columns,
16218 on_delete,
16219 on_update,
16220 match_type,
16221 deferrable,
16222 initially_deferred,
16223 ) = self.parse_references_tail(columns.len())?;
16224 Ok(ForeignKeyConstraint {
16225 name,
16226 columns,
16227 parent_table,
16228 parent_columns,
16229 on_delete,
16230 on_update,
16231 match_type,
16232 deferrable,
16233 initially_deferred,
16234 })
16235 }
16236
16237 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16238 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16239 /// the local column count, used to default the parent column
16240 /// list when omitted (SQL spec: parent's PK is implied).
16241 fn parse_references_tail(
16242 &mut self,
16243 expected_arity: usize,
16244 ) -> Result<
16245 (
16246 String,
16247 Vec<String>,
16248 FkAction,
16249 FkAction,
16250 crate::ast::MatchType,
16251 // v7.39 (round 288) — deferrable, initially_deferred.
16252 bool,
16253 bool,
16254 ),
16255 ParseError,
16256 > {
16257 match self.advance() {
16258 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16259 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16260 }
16261 let parent_table = self.expect_ident_like()?;
16262 let mut parent_columns: Vec<String> = Vec::new();
16263 if matches!(self.peek(), Token::LParen) {
16264 self.advance();
16265 loop {
16266 parent_columns.push(self.expect_ident_like()?);
16267 match self.peek() {
16268 Token::Comma => {
16269 self.advance();
16270 }
16271 Token::RParen => {
16272 self.advance();
16273 break;
16274 }
16275 other => {
16276 return Err(self.err(format!(
16277 "expected ',' or ')' in REFERENCES column list, got {other:?}"
16278 )));
16279 }
16280 }
16281 }
16282 }
16283 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16284 return Err(self.err(format!(
16285 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16286 expected_arity,
16287 parent_columns.len()
16288 )));
16289 }
16290 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16291 // it between the referenced column list and the ON / DEFERRABLE
16292 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16293 // is skipped when any referencing column is NULL), so SIMPLE —
16294 // the default, and the only spelling pg_dump emits — is accepted
16295 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16296 // mixed-NULL rule, which is not wired yet; reject them honestly
16297 // rather than silently applying SIMPLE (PG itself errors on
16298 // MATCH PARTIAL as "not yet implemented").
16299 let mut match_type = crate::ast::MatchType::Simple;
16300 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16301 self.advance();
16302 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16303 // SIMPLE / PARTIAL arrive as bare identifiers.
16304 let kind = match self.advance() {
16305 Token::Full => "FULL".to_string(),
16306 Token::Ident(s) => s.to_uppercase(),
16307 other => {
16308 return Err(self.err(format!(
16309 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16310 )));
16311 }
16312 };
16313 match kind.as_str() {
16314 "SIMPLE" => {} // Default — match_type stays Simple.
16315 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16316 // when ALL referencing columns are NULL; a mixed-NULL key errors.
16317 "FULL" => match_type = crate::ast::MatchType::Full,
16318 "PARTIAL" => {
16319 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16320 }
16321 _ => {
16322 return Err(self.err(format!(
16323 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16324 )));
16325 }
16326 }
16327 }
16328 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16329 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16330 // <action>` / `ON UPDATE <action>` in either order. PG /
16331 // pg_dump emits the timing clause AFTER the ON clauses
16332 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16333 // but the SQL spec allows either order. We loop over
16334 // every possible trailer and dispatch on the next token,
16335 // stopping when nothing matches. Phase 3.1 changes the
16336 // bare DEFERRABLE form from hard-error to accept-as-
16337 // immediate; SPG is single-writer with no deferred-
16338 // constraint window so the runtime semantics are always
16339 // immediate even when INITIALLY DEFERRED is requested.
16340 // PG's default referential action (no ON DELETE / ON UPDATE
16341 // clause) is NO ACTION, not RESTRICT — the two enforce
16342 // identically in SPG (single-writer, no deferred window; see the
16343 // shared match arm in constraints.rs) but information_schema.
16344 // referential_constraints must report NO ACTION to match PG.
16345 let mut on_delete = FkAction::NoAction;
16346 let mut on_update = FkAction::NoAction;
16347 let mut seen_on_delete = false;
16348 let mut seen_on_update = false;
16349 let mut deferrable = false;
16350 let mut initially_deferred = false;
16351 loop {
16352 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16353 let before = self.pos;
16354 let (d, idef) = self.consume_deferrable_clauses_timed()?;
16355 if self.pos != before {
16356 deferrable = d;
16357 initially_deferred = idef;
16358 continue;
16359 }
16360 // ON DELETE / ON UPDATE.
16361 if !matches!(self.peek(), Token::On) {
16362 break;
16363 }
16364 self.advance();
16365 let which = self.advance();
16366 let action = self.parse_fk_action()?;
16367 match which {
16368 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16369 if seen_on_delete {
16370 return Err(self.err("ON DELETE specified twice".into()));
16371 }
16372 seen_on_delete = true;
16373 on_delete = action;
16374 }
16375 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16376 if seen_on_update {
16377 return Err(self.err("ON UPDATE specified twice".into()));
16378 }
16379 seen_on_update = true;
16380 on_update = action;
16381 }
16382 other => {
16383 return Err(
16384 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16385 );
16386 }
16387 }
16388 }
16389 Ok((
16390 parent_table,
16391 parent_columns,
16392 on_delete,
16393 on_update,
16394 match_type,
16395 deferrable,
16396 initially_deferred,
16397 ))
16398 }
16399
16400 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16401 /// NO ACTION`.
16402 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16403 match self.advance() {
16404 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16405 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16406 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16407 Token::Null => Ok(FkAction::SetNull),
16408 Token::Default => Ok(FkAction::SetDefault),
16409 other => Err(self.err(format!(
16410 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16411 ))),
16412 },
16413 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16414 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16415 other => Err(self.err(format!(
16416 "expected ACTION after NO in FK action, got {other:?}"
16417 ))),
16418 },
16419 other => Err(self.err(format!(
16420 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16421 ))),
16422 }
16423 }
16424
16425 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16426 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16427 fn consume_if_not_exists(&mut self) -> bool {
16428 // `IF` arrives as a bare Ident (we don't reserve it because it
16429 // also appears mid-expression in PG, though we don't support
16430 // those forms yet).
16431 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16432 if !looks_like_if {
16433 return false;
16434 }
16435 // Peek one ahead before committing: only consume IF when it's
16436 // actually `IF NOT EXISTS`.
16437 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16438 return false;
16439 }
16440 if !matches!(
16441 self.tokens.get(self.pos + 2),
16442 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16443 ) {
16444 return false;
16445 }
16446 self.advance(); // IF
16447 self.advance(); // NOT
16448 self.advance(); // EXISTS
16449 true
16450 }
16451
16452 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16453 /// Consumes IF EXISTS as a pair; returns false otherwise
16454 /// without consuming any tokens.
16455 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16456 /// ENABLE/DISABLE/FORCE/NO FORCE.
16457 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16458 for kw in ["row", "level", "security"] {
16459 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16460 {
16461 return Err(self.err(alloc::format!(
16462 "expected {} in ROW LEVEL SECURITY, got {:?}",
16463 kw.to_ascii_uppercase(),
16464 self.peek()
16465 )));
16466 }
16467 self.advance();
16468 }
16469 Ok(())
16470 }
16471
16472 fn consume_if_exists(&mut self) -> bool {
16473 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16474 if !looks_like_if {
16475 return false;
16476 }
16477 if !matches!(
16478 self.tokens.get(self.pos + 1),
16479 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16480 ) {
16481 return false;
16482 }
16483 self.advance(); // IF
16484 self.advance(); // EXISTS
16485 true
16486 }
16487
16488 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16489 /// qualifiers after an index column ref. ASC / DESC are
16490 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16491 /// We accept and discard them since single-column BTree
16492 /// stores rows in natural key order today.
16493 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16494 /// ORDER BY key. Returns None when absent.
16495 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16496 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16497 return Ok(None);
16498 }
16499 self.advance();
16500 match self.advance() {
16501 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16502 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16503 other => Err(self.err(alloc::format!(
16504 "expected FIRST or LAST after NULLS, got {other:?}"
16505 ))),
16506 }
16507 }
16508
16509 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16510 /// rather than discarded.
16511 ///
16512 /// SPG's index does not scan in a direction — column ordering is
16513 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16514 /// reproduction of the DDL, and dropping the clause meant
16515 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16516 /// dump lost it, and a schema diff saw drift on every run.
16517 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16518 let mut order = crate::ast::IndexColumnOrder::default();
16519 loop {
16520 match self.peek() {
16521 Token::Asc => {
16522 self.advance();
16523 }
16524 Token::Desc => {
16525 order.descending = true;
16526 self.advance();
16527 }
16528 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16529 let look = self.tokens.get(self.pos + 1);
16530 if matches!(
16531 look,
16532 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16533 || k.eq_ignore_ascii_case("last")
16534 ) {
16535 self.advance();
16536 order.nulls_first = Some(matches!(
16537 self.advance(),
16538 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16539 ));
16540 } else {
16541 break;
16542 }
16543 }
16544 _ => break,
16545 }
16546 }
16547 order
16548 }
16549
16550 fn parse_create_index_stmt_after_create(
16551 &mut self,
16552 is_unique: bool,
16553 ) -> Result<Statement, ParseError> {
16554 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16555 debug_assert!(matches!(self.peek(), Token::Index));
16556 self.advance();
16557 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16558 // SPG's CREATE INDEX is synchronous end-to-end today (real
16559 // CONCURRENTLY variant with restartable scans queues with
16560 // v7.39 indexes epic), so the modifier has no runtime effect
16561 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16562 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16563 // VIEW CONCURRENTLY.
16564 let mut concurrently = false;
16565 if matches!(
16566 self.peek(),
16567 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16568 ) {
16569 self.advance();
16570 concurrently = true;
16571 }
16572 let if_not_exists = self.consume_if_not_exists();
16573 // v7.39 (read01 round 93) — the index name is optional (PG since
16574 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16575 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16576 // was given; leave it empty and the engine derives a PG-style
16577 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16578 let name = if matches!(self.peek(), Token::On) {
16579 String::new()
16580 } else {
16581 self.expect_ident_like()?
16582 };
16583 if !matches!(self.peek(), Token::On) {
16584 return Err(self.err(format!(
16585 "expected ON after CREATE INDEX <name>, got {:?}",
16586 self.peek()
16587 )));
16588 }
16589 self.advance();
16590 let table = self.expect_ident_like()?;
16591 // Optional `USING <method>` — only recognised method in v2.0 is
16592 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16593 // ident `using` (we don't promote it to a reserved keyword
16594 // because it isn't reserved anywhere else in our SQL surface).
16595 let mut method_name: Option<String> = None;
16596 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16597 self.advance();
16598 let m = self.expect_ident_like()?;
16599 method_name = Some(m.to_ascii_lowercase());
16600 match m.to_ascii_lowercase().as_str() {
16601 "hnsw" => IndexMethod::Hnsw,
16602 "btree" => IndexMethod::BTree,
16603 "brin" => IndexMethod::Brin,
16604 // v7.12.3 — real GIN inverted index over `tsvector`.
16605 // v7.9.26b's `USING gin` → BTree silent fallback is
16606 // gone; the engine validates that the indexed column
16607 // is `tsvector` at CREATE INDEX time.
16608 "gin" => IndexMethod::Gin,
16609 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16610 // `USING spgist` / `USING hash` for their built-in
16611 // AMs that SPG doesn't have a matching
16612 // implementation for; degrade to BTree on the
16613 // leading column so the schema loads + the index
16614 // catalogue stays consistent. Operator pays the
16615 // planner cost only for the queries that would have
16616 // used the specialised AM.
16617 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16618 // v7.11.3 — pgvector ships both `ivfflat` and
16619 // `hnsw`. Customers shouldn't have to choose
16620 // their on-disk index method based on what SPG
16621 // implements; accept `ivfflat` as a synonym for
16622 // `hnsw` so PG schemas using either method drop
16623 // in. The vector distance op (`<->` / `<#>` /
16624 // `<=>`) at query time still picks the metric.
16625 "ivfflat" => IndexMethod::Hnsw,
16626 other => {
16627 return Err(self.err(alloc::format!(
16628 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16629 )));
16630 }
16631 }
16632 } else {
16633 IndexMethod::BTree
16634 };
16635 if !matches!(self.peek(), Token::LParen) {
16636 return Err(self.err(format!(
16637 "expected '(' before indexed column, got {:?}",
16638 self.peek()
16639 )));
16640 }
16641 self.advance();
16642 // v6.8.2 — accept either a bare column ident (legacy) or
16643 // an expression `fn(col, …)` for expression indexes.
16644 // Distinguish by peeking the token *after* the current
16645 // ident: `ident )` is the legacy column-only path;
16646 // anything else triggers the Pratt expression parser.
16647 // (`advance()` uses `mem::replace` to nil out the current
16648 // slot, so we can't save+rewind cleanly — peek-ahead via
16649 // direct index avoids the mutation.)
16650 let mut opclass: Option<String> = None;
16651 let mut key_collation: Option<String> = None;
16652 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16653 // Single column with `)` immediately after — fast path.
16654 // v7.9.29 — also: bare column followed by `,` (the
16655 // multi-column form `(a, b, c)`). Without this branch
16656 // the leading ident gets pulled into `parse_expr`
16657 // which then sets `expression = Some(Column(a))` and
16658 // breaks Display round-trip on the multi-column shape.
16659 Token::Ident(s) | Token::QuotedIdent(s)
16660 if matches!(
16661 self.tokens.get(self.pos + 1),
16662 Some(Token::RParen | Token::Comma)
16663 ) =>
16664 {
16665 self.advance();
16666 (s, None)
16667 }
16668 // v7.9.22 — single column followed by a pgvector
16669 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16670 // v7.15.0 — capture the opclass instead of discarding
16671 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16672 // → real trigram-shingle GIN over a TEXT column).
16673 // Vector/HNSW opclasses still take their distance
16674 // metric from the query operator (`<->` / `<#>` /
16675 // `<=>`), so for those callers the opclass stays
16676 // informational.
16677 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16678 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16679 // the schema and dispatch on the bare opclass, the same
16680 // treatment table/type names get.
16681 Token::Ident(s) | Token::QuotedIdent(s)
16682 if matches!(
16683 self.tokens.get(self.pos + 1),
16684 Some(Token::Ident(_) | Token::QuotedIdent(_))
16685 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16686 && matches!(
16687 self.tokens.get(self.pos + 3),
16688 Some(Token::Ident(op) | Token::QuotedIdent(op))
16689 if is_vector_opclass_name(op)
16690 ) =>
16691 {
16692 self.advance(); // column name
16693 self.advance(); // schema qualifier
16694 self.advance(); // dot
16695 let op_tok = self.advance();
16696 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16697 opclass = Some(op.to_ascii_lowercase());
16698 }
16699 (s, None)
16700 }
16701 // r1038 — an operator class is recognised by its POSITION, not
16702 // by a list of names. It used to be `is_vector_opclass_name`,
16703 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16704 // sentori's migration wrote — was a syntax error while
16705 // `USING gin (doc)` parsed. Anything sitting between a column
16706 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16707 // two bare identifiers in a row are not valid there otherwise.
16708 Token::Ident(s) | Token::QuotedIdent(s)
16709 if matches!(
16710 self.tokens.get(self.pos + 1),
16711 Some(Token::Ident(op) | Token::QuotedIdent(op))
16712 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16713 self.tokens.get(self.pos + 2)
16714 )
16715 ) =>
16716 {
16717 self.advance(); // column name
16718 // Capture the opclass token, lower-cased for
16719 // case-insensitive engine dispatch.
16720 let op_tok = self.advance();
16721 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16722 opclass = Some(op.to_ascii_lowercase());
16723 }
16724 (s, None)
16725 }
16726 Token::Ident(_) | Token::QuotedIdent(_) => {
16727 // v7.39 (round 538) — an explicit COLLATE on the key,
16728 // read by LOOKAHEAD because `parse_expr` absorbs the
16729 // clause as a no-op (SPG orders text by bytes, which is
16730 // the C collation, so it changes nothing to honour). PG
16731 // still PRINTS it: an explicitly written `"C"` and the
16732 // collation a column inherits are different collation
16733 // OBJECTS even where they sort identically, which is why
16734 // `(a COLLATE "C")` shows on a C-collation database too.
16735 if matches!(
16736 self.tokens.get(self.pos + 1),
16737 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16738 ) {
16739 key_collation = match self.tokens.get(self.pos + 2) {
16740 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16741 Some(n.clone())
16742 }
16743 _ => None,
16744 };
16745 }
16746 // v7.39.2 — the clause is read by the LOOKAHEAD above and
16747 // belongs to the KEY, not to the expression. Since
16748 // `COLLATE` became a node, letting `parse_expr` build one
16749 // here put the collation in twice and the key deparsed as
16750 // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16751 // is the same idea and already exists, so this borrows it:
16752 // absorb into the side channel, and the key's own
16753 // lookahead is what carries it.
16754 // v7.39.2 — and the key can only CARRY the byte-order
16755 // spellings. Absorbing into the side channel accepts any
16756 // name, so suppressing the node here without this check
16757 // silently accepted `(name COLLATE "en_US")`, which SPG's
16758 // index cannot honour — a refusal that was doing real
16759 // work, removed by the suppression and put back here.
16760 if let Some(name) = &key_collation {
16761 let lc = name.to_ascii_lowercase();
16762 let byte_order = matches!(
16763 lc.as_str(),
16764 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16765 );
16766 let mysql_ok = self.mysql_dialect
16767 && (lc.ends_with("_ci")
16768 || lc.ends_with("_bin")
16769 || lc == "binary"
16770 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16771 if !byte_order && !mysql_ok {
16772 return Err(self.err(alloc::format!(
16773 "COLLATE {name:?} is not supported in this position: an index \
16774 key carries the byte-order spellings only. Declare it on the \
16775 column (`x text COLLATE {name:?}`) instead"
16776 )));
16777 }
16778 }
16779 let saved_key_ctx = self.in_order_by_key;
16780 self.in_order_by_key = true;
16781 let key_expr = self.parse_expr(0);
16782 self.in_order_by_key = saved_key_ctx;
16783 let key_expr = key_expr?;
16784 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16785 self.err("expression index key must reference at least one column".into())
16786 })?;
16787 (primary, Some(key_expr))
16788 }
16789 // v7.37.43-T4 — parenthesised expression index key
16790 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16791 // PG's CREATE INDEX requires the expression to be in
16792 // its own parens to disambiguate function calls from
16793 // column lists, so this `LParen` is the inner open-paren
16794 // of an expression key. parse_expr handles the recursive
16795 // descent and consumes the matching `RParen`.
16796 Token::LParen => {
16797 let key_expr = self.parse_expr(0)?;
16798 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16799 self.err("expression index key must reference at least one column".into())
16800 })?;
16801 (primary, Some(key_expr))
16802 }
16803 other => {
16804 return Err(self.err(format!(
16805 "expected column ident or expression, got {other:?}"
16806 )));
16807 }
16808 };
16809 // v7.9.14 — accept extra comma-separated columns inside
16810 // the index key parens (`CREATE INDEX … (a, b, c)`).
16811 // mailrs F2.
16812 //
16813 // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16814 // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16815 // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16816 // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16817 // schema diff saw drift on every run. Reported by sentori
16818 // against 7.39.10, and the same defect round 537 fixed for the
16819 // LEADING column, in the loop right beside it.
16820 let mut extra_columns: Vec<String> = Vec::new();
16821 let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16822 // The leading column may also have ASC/DESC after it — and that
16823 // one is the column SPG indexes, so its clause is kept.
16824 let key_order = self.consume_optional_index_column_qualifiers();
16825 while matches!(self.peek(), Token::Comma) {
16826 self.advance();
16827 let extra = self.expect_ident_like()?;
16828 extra_orders.push(self.consume_optional_index_column_qualifiers());
16829 extra_columns.push(extra);
16830 }
16831 if !matches!(self.peek(), Token::RParen) {
16832 return Err(self.err(format!(
16833 "expected ')' after indexed column / expression, got {:?}",
16834 self.peek()
16835 )));
16836 }
16837 self.advance();
16838 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16839 // index-only-scan annotation. Bare ident (not a reserved
16840 // keyword) so we test by case-insensitive string match.
16841 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16842 {
16843 self.advance();
16844 if !matches!(self.peek(), Token::LParen) {
16845 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16846 }
16847 self.advance();
16848 let mut cols = Vec::new();
16849 loop {
16850 cols.push(self.expect_ident_like()?);
16851 match self.peek() {
16852 Token::Comma => {
16853 self.advance();
16854 }
16855 Token::RParen => {
16856 self.advance();
16857 break;
16858 }
16859 other => {
16860 return Err(self.err(format!(
16861 "expected ',' or ')' in INCLUDE list, got {other:?}"
16862 )));
16863 }
16864 }
16865 }
16866 cols
16867 } else {
16868 Vec::new()
16869 };
16870 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16871 // storage parameters. pgvector emits `WITH (lists = N)` for
16872 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16873 // SPG's HNSW picks its own parameters today (tunable via
16874 // env vars), so the WITH clause is informational and dropped.
16875 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16876 self.advance();
16877 if !matches!(self.peek(), Token::LParen) {
16878 return Err(self.err(format!(
16879 "expected '(' after WITH in CREATE INDEX, got {:?}",
16880 self.peek()
16881 )));
16882 }
16883 self.advance();
16884 loop {
16885 if matches!(self.peek(), Token::RParen) {
16886 self.advance();
16887 break;
16888 }
16889 // Drain `key = value` or bare `key` tokens.
16890 let _ = self.advance(); // key
16891 if matches!(self.peek(), Token::Eq) {
16892 self.advance();
16893 let _ = self.advance(); // value (int / string / ident)
16894 }
16895 match self.peek() {
16896 Token::Comma => {
16897 self.advance();
16898 }
16899 Token::RParen => {
16900 self.advance();
16901 break;
16902 }
16903 other => {
16904 return Err(self.err(format!(
16905 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16906 )));
16907 }
16908 }
16909 }
16910 }
16911 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16912 // which sits between the key list and the WHERE clause.
16913 let mut nulls_not_distinct = false;
16914 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16915 let n1 = self.tokens.get(self.pos + 1);
16916 let n2 = self.tokens.get(self.pos + 2);
16917 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16918 self.advance(); // NULLS
16919 self.advance(); // NOT
16920 self.advance(); // DISTINCT
16921 nulls_not_distinct = true;
16922 } else if matches!(n1, Some(Token::Distinct)) {
16923 self.advance(); // NULLS
16924 self.advance(); // DISTINCT
16925 }
16926 }
16927 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16928 let partial_predicate = if matches!(self.peek(), Token::Where) {
16929 self.advance();
16930 Some(self.parse_expr(0)?)
16931 } else {
16932 None
16933 };
16934 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16935 // sense: uniqueness over an ANN structure has no clean
16936 // semantics. Reject early. (BRIN UNIQUE is similarly
16937 // meaningless — block both.)
16938 if is_unique && !matches!(method, IndexMethod::BTree) {
16939 return Err(self.err(alloc::format!(
16940 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16941 method
16942 )));
16943 }
16944 Ok(Statement::CreateIndex(CreateIndexStatement {
16945 concurrently,
16946 name,
16947 key_order,
16948 key_collation,
16949 table,
16950 column,
16951 nulls_not_distinct,
16952 method,
16953 if_not_exists,
16954 included_columns,
16955 partial_predicate,
16956 extra_columns: extra_columns.clone(),
16957 extra_orders: extra_orders.clone(),
16958 expression,
16959 is_unique,
16960 opclass,
16961 method_name,
16962 }))
16963 }
16964
16965 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16966 /// column-level `REFERENCES ...` clause. The trailing FK is
16967 /// normalised into table-level shape (single-element columns +
16968 /// parent_columns) so the engine sees one uniform constraint list.
16969 fn parse_column_def_with_fk(
16970 &mut self,
16971 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16972 let col = self.parse_column_def()?;
16973 // v7.39 (round 308, V29) — an explicitly named inline FK:
16974 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16975 // loop leaves this spelling intact precisely so the name can be
16976 // kept here; PG reports it in violation messages and matches it
16977 // in `SET CONSTRAINTS`.
16978 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16979 {
16980 self.advance();
16981 Some(self.expect_ident_like()?)
16982 } else {
16983 None
16984 };
16985 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16986 let inline_references = matches!(
16987 self.peek(),
16988 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16989 );
16990 if !inline_references {
16991 return Ok((col, None));
16992 }
16993 let (
16994 parent_table,
16995 parent_columns,
16996 on_delete,
16997 on_update,
16998 match_type,
16999 deferrable,
17000 initially_deferred,
17001 ) = self.parse_references_tail(1)?;
17002 let fk = ForeignKeyConstraint {
17003 name: declared_name,
17004 columns: vec![col.name.clone()],
17005 parent_table,
17006 parent_columns,
17007 on_delete,
17008 on_update,
17009 match_type,
17010 deferrable,
17011 initially_deferred,
17012 };
17013 Ok((col, Some(fk)))
17014 }
17015
17016 /// v7.13.0 — parse a column type (consuming the type ident and
17017 /// any trailing parameters / `[]`), without surrounding column
17018 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
17019 /// Returns the resolved `ColumnTypeName` plus implied
17020 /// `(auto_increment, not_null)` flags from PG SERIAL family
17021 /// shorthands — callers that don't expect those (ALTER COLUMN
17022 /// TYPE) can discard them.
17023 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
17024 let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
17025 Ok(ty)
17026 }
17027
17028 #[allow(clippy::type_complexity)]
17029 fn parse_type_with_implied_flags(
17030 &mut self,
17031 ) -> Result<
17032 (
17033 ColumnTypeName,
17034 bool,
17035 bool,
17036 Option<String>,
17037 Collation,
17038 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
17039 bool,
17040 // v7.39 (round 676) — the collation NAME as written, which the
17041 // `Collation` enum above cannot carry.
17042 Option<String>,
17043 bool,
17044 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
17045 // list captured at type-parse time. None for all
17046 // non-ENUM types.
17047 Option<Vec<String>>,
17048 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
17049 // list. Distinct from ENUM (subset semantics).
17050 Option<Vec<String>>,
17051 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
17052 // width, lost when the type collapses to SmallInt / Int.
17053 Option<MysqlIntWidth>,
17054 // v7.39 (round 424) — declared fractional-seconds precision of a
17055 // MySQL temporal column (bare spelling = 0). None under PG.
17056 Option<u8>,
17057 // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
17058 // two are different types on MySQL and SPG stores both as
17059 // `Timestamp`, so the spelling has to travel separately or
17060 // a dump silently rewrites the column.
17061 bool,
17062 // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
17063 // display hint: it rounds on write.
17064 Option<(u8, u8)>,
17065 ),
17066 ParseError,
17067 > {
17068 let mut ty_ident = match self.advance() {
17069 Token::Ident(s) => s,
17070 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
17071 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
17072 // '<span>'` literal grammar. As a column type it lands
17073 // here directly; downstream resolution still uses the
17074 // canonical lowercase string.
17075 Token::Interval => "interval".to_string(),
17076 other => {
17077 return Err(ParseError {
17078 message: format!("expected column type, got {other:?}"),
17079 token_pos: self.consumed_pos(),
17080 });
17081 }
17082 };
17083 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
17084 // pg_dump qualifies extension types (`public.vector(1024)`).
17085 // SPG is single-namespace; drop the schema and resolve the
17086 // bare type — same treatment table names already get.
17087 while matches!(self.peek(), Token::Dot) {
17088 self.advance();
17089 ty_ident = self.expect_ident_like()?;
17090 }
17091 let mut implied_auto_increment = false;
17092 let mut implied_not_null = false;
17093 let mut user_type_ref: Option<String> = None;
17094 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17095 // value list, captured here and bubbled up through the
17096 // ColumnDef so the engine can attach it to the column
17097 // schema (and validate INSERT cells against it).
17098 let mut inline_enum_variants: Option<Vec<String>> = None;
17099 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17100 let mut inline_set_variants: Option<Vec<String>> = None;
17101 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17102 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17103 // collapses to SmallInt / Int. Only under the MySQL dialect.
17104 let mut mysql_int_width: Option<MysqlIntWidth> = None;
17105 // v7.39 (round 424) — the declared fractional-seconds precision of a
17106 // MySQL temporal column. Set by the temporal arms below; stays None
17107 // for PG (whose temporal columns keep full microseconds).
17108 let mut mysql_fsp: Option<u8> = None;
17109 let mut mysql_declared_timestamp = false;
17110 let mut mysql_float_md: Option<(u8, u8)> = None;
17111 let mut ty = match ty_ident.as_str() {
17112 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17113 "smallserial" | "serial2" => {
17114 implied_auto_increment = true;
17115 implied_not_null = true;
17116 ColumnTypeName::SmallInt
17117 }
17118 "serial" | "serial4" => {
17119 implied_auto_increment = true;
17120 implied_not_null = true;
17121 ColumnTypeName::Int
17122 }
17123 "bigserial" | "serial8" => {
17124 implied_auto_increment = true;
17125 implied_not_null = true;
17126 ColumnTypeName::BigInt
17127 }
17128 // MySQL flavours we accept by aliasing to the closest SPG
17129 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17130 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17131 // 24-bit) → INT. UNSIGNED modifiers are consumed below
17132 // without semantic effect.
17133 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17134 // PG's internal type names; pg_dump and hand-written PG schemas
17135 // use them interchangeably with smallint / int / bigint (the cast
17136 // path already accepted them, only the column grammar didn't).
17137 "smallint" | "int2" => {
17138 // v7.14.0 — MySQL display-width on integers
17139 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17140 // parenthesised number is purely cosmetic — it
17141 // doesn't change storage. Accept + discard.
17142 self.consume_optional_paren_size();
17143 ColumnTypeName::SmallInt
17144 }
17145 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17146 // canonical encoding for BOOLEAN. Every MySQL driver
17147 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17148 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17149 // 4.3 SPG classified TINYINT(1) as SmallInt, which
17150 // gave the customer i16-shaped values where the app
17151 // expected bool — a Tier-A silent type drift on
17152 // mysqldump restores. Now: `TINYINT(1)` → Bool;
17153 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17154 // stay SmallInt (the legacy width-agnostic path).
17155 "tinyint" => {
17156 let width = self.peek_optional_paren_size_value();
17157 self.consume_optional_paren_size();
17158 if width == Some(1) {
17159 ColumnTypeName::Bool
17160 } else {
17161 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17162 // lost width so the write path can enforce -128..127.
17163 if self.mysql_dialect {
17164 mysql_int_width = Some(MysqlIntWidth::Tiny);
17165 }
17166 ColumnTypeName::SmallInt
17167 }
17168 }
17169 "mediumint" => {
17170 self.consume_optional_paren_size();
17171 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17172 if self.mysql_dialect {
17173 mysql_int_width = Some(MysqlIntWidth::Medium);
17174 }
17175 ColumnTypeName::Int
17176 }
17177 "int" | "integer" | "int4" => {
17178 self.consume_optional_paren_size();
17179 ColumnTypeName::Int
17180 }
17181 "bigint" | "int8" => {
17182 self.consume_optional_paren_size();
17183 ColumnTypeName::BigInt
17184 }
17185 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17186 // (mailrs round-5 G6). Consume the optional `PRECISION`
17187 // tail when the type keyword was `double` / `DOUBLE`.
17188 //
17189 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17190 // FLOAT". `FLOAT(p)` picks the width the way PG does:
17191 // p in 1..=24 is real, 25..=53 is double precision, and
17192 // anything else is an error.
17193 "float" | "double" | "real" => {
17194 if ty_ident.eq_ignore_ascii_case("double")
17195 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17196 {
17197 self.advance();
17198 }
17199 if ty_ident.eq_ignore_ascii_case("real") {
17200 // v7.39 (round 274) — the two dialects genuinely
17201 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17202 // synonym for DOUBLE (8-byte). Round 269 made REAL
17203 // 32-bit globally and thereby narrowed the stored
17204 // precision of every MySQL REAL column.
17205 if self.mysql_dialect {
17206 ColumnTypeName::Float
17207 } else {
17208 ColumnTypeName::Real
17209 }
17210 } else if self.mysql_dialect
17211 && matches!(self.peek(), Token::LParen)
17212 && self.peek_paren_has_comma()
17213 {
17214 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17215 // display form (`FLOAT(10,2)`), which PG has no
17216 // equivalent of. It was `syntax error at or near ","`,
17217 // so the whole CREATE failed.
17218 //
17219 // v7.39.2 — the guard said `float` while the comment
17220 // said both, so `DOUBLE(10,2)` — which every legacy
17221 // MySQL schema uses for money — still failed the
17222 // whole CREATE with `syntax error at or near "("`.
17223 // Measured on 9.7.2: both forms are accepted, and the
17224 // digits are NOT a display hint, they round on write
17225 // (3.14159265358979 into either stores 3.14). The
17226 // rounding is recorded as a residual; accepting the
17227 // syntax and keeping the width is the half this
17228 // change makes.
17229 // v7.39.3 — keep the pair. The digits are not a
17230 // display hint: MySQL 9.7.2 ROUNDS on write and
17231 // refuses a value wider than `m` (errno 1264), so a
17232 // column declared for money held more precision here
17233 // than its schema said.
17234 let (m, d) = self.parse_optional_numeric_params()?;
17235 mysql_float_md = Some((
17236 u8::try_from(m).unwrap_or(u8::MAX),
17237 u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17238 ));
17239 if ty_ident.eq_ignore_ascii_case("float") {
17240 ColumnTypeName::Real
17241 } else {
17242 ColumnTypeName::Float
17243 }
17244 } else if ty_ident.eq_ignore_ascii_case("float")
17245 && matches!(self.peek(), Token::LParen)
17246 {
17247 // PG words the two bounds differently, and
17248 // parse_paren_size already rejects a zero.
17249 let p = self.parse_paren_size("FLOAT")?;
17250 if p > 53 {
17251 return Err(self.err(String::from(
17252 "precision for type float must be less than 54 bits",
17253 )));
17254 }
17255 if p <= 24 {
17256 ColumnTypeName::Real
17257 } else {
17258 ColumnTypeName::Float
17259 }
17260 } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17261 // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17262 // eight (it is `float8`'s spelling there). SPG used
17263 // PG's for both, so a MySQL FLOAT column silently
17264 // kept more precision than MySQL does — measured,
17265 // 3.14159265358979 comes back as 3.14159 there and
17266 // came back whole here — and reported itself as
17267 // `double` to every reflection.
17268 //
17269 // This is the mirror of the REAL split above: the
17270 // two dialects disagree about which spelling means
17271 // which width, and one of them was already honoured.
17272 ColumnTypeName::Real
17273 } else {
17274 ColumnTypeName::Float
17275 }
17276 }
17277 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17278 "float4" => ColumnTypeName::Real,
17279 "float8" => ColumnTypeName::Float,
17280 "text" => ColumnTypeName::Text,
17281 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17282 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17283 // real MySQL schema and NONE of them existed: the CREATE
17284 // failed outright with `type "blob" does not exist`, so the
17285 // table was never made. The sizes differ only in MySQL's
17286 // maximum length, which SPG does not cap, so they collapse
17287 // onto TEXT and BYTEA the way the unsized spellings do.
17288 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17289 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17290 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17291 // enforce, consumed so the declaration parses.
17292 "varbinary" | "binary" => {
17293 self.consume_optional_paren_size();
17294 ColumnTypeName::Bytes
17295 }
17296 "name" => ColumnTypeName::Name,
17297 "xid" => ColumnTypeName::Xid,
17298 "oid" => ColumnTypeName::Oid,
17299 "xid8" => ColumnTypeName::Xid8,
17300 "bool" | "boolean" => ColumnTypeName::Bool,
17301 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17302 // an unbounded `character varying`, which the arm below has always
17303 // read as text. Only the short spelling demanded a length, so
17304 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17305 // there is — failed on `VARCHAR type requires (N)` while the long
17306 // spelling of the same thing was accepted. The same asymmetry
17307 // round 613 closed on the CAST side, here on the DDL side.
17308 "varchar" => {
17309 if matches!(self.peek(), Token::LParen) {
17310 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17311 } else {
17312 ColumnTypeName::Text
17313 }
17314 }
17315 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17316 // `character` below (SQL standard).
17317 "char" => {
17318 if matches!(self.peek(), Token::LParen) {
17319 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17320 } else {
17321 ColumnTypeName::Char(1)
17322 }
17323 }
17324 // pg_dump's canonical spellings: `character varying(n)` = varchar,
17325 // `character(n)` = char, bare `character` = char(1). Unbounded
17326 // `character varying` maps to text.
17327 "character" => {
17328 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17329 self.advance();
17330 if matches!(self.peek(), Token::LParen) {
17331 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17332 } else {
17333 ColumnTypeName::Text
17334 }
17335 } else if matches!(self.peek(), Token::LParen) {
17336 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17337 } else {
17338 ColumnTypeName::Char(1)
17339 }
17340 }
17341 "vector" => {
17342 let dim = self.parse_paren_size("VECTOR")?;
17343 let encoding = self.parse_optional_vector_encoding()?;
17344 ColumnTypeName::Vector { dim, encoding }
17345 }
17346 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17347 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17348 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17349 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17350 // DECIMAL(10,2))` — how nearly every money column is written,
17351 // in either dialect — was a syntax error and the table was
17352 // never created. `FIXED` is MySQL's alias alone, so it is
17353 // taken only in that dialect.
17354 "numeric" | "decimal" | "dec" => {
17355 let (precision, scale) = self.parse_optional_numeric_params()?;
17356 ColumnTypeName::Numeric(precision, scale)
17357 }
17358 "fixed" if self.mysql_dialect => {
17359 let (precision, scale) = self.parse_optional_numeric_params()?;
17360 ColumnTypeName::Numeric(precision, scale)
17361 }
17362 "date" => ColumnTypeName::Date,
17363 // MySQL's `DATETIME` is the same domain as standard
17364 // `TIMESTAMP` — accept both spellings.
17365 "timestamp" | "datetime" => {
17366 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17367 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17368 // TIME ZONE` clause, so consume it first.
17369 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17370 // (it truncates on write and pads on render), so capture it;
17371 // a bare spelling means precision 0 there. PG stores µs always
17372 // and keeps `None`.
17373 let n = self.take_optional_paren_size();
17374 if self.mysql_dialect {
17375 mysql_fsp = Some(n.unwrap_or(0).min(6));
17376 // v7.39.2 — remember WHICH spelling was written.
17377 // MySQL and MariaDB keep `timestamp` and `datetime`
17378 // apart everywhere a client can read the type back,
17379 // and SPG reported `datetime` for both — so a dump
17380 // and reload silently changed the column's declared
17381 // type, and MySQL's TIMESTAMP is not DATETIME (a
17382 // different range, and UTC conversion on the way in
17383 // and out).
17384 mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17385 }
17386 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17387 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17388 // the full form. SPG canonicalises:
17389 // - WITH TIME ZONE → Timestamptz
17390 // - WITHOUT TIME ZONE → Timestamp
17391 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17392 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17393 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17394 {
17395 self.advance(); // WITH
17396 self.advance(); // TIME
17397 self.advance(); // ZONE
17398 ColumnTypeName::Timestamptz
17399 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17400 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17401 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17402 {
17403 self.advance(); // WITHOUT
17404 self.advance(); // TIME
17405 self.advance(); // ZONE
17406 ColumnTypeName::Timestamp
17407 } else {
17408 // A second `(precision)` cannot legally follow, but the
17409 // old grammar tolerated it; keep that tolerance.
17410 self.consume_optional_paren_size();
17411 ColumnTypeName::Timestamp
17412 }
17413 }
17414 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17415 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17416 // only PG-wire OID differs.
17417 "timestamptz" => {
17418 self.consume_optional_paren_size();
17419 ColumnTypeName::Timestamptz
17420 }
17421 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17422 // validation. We accept the JSONB spelling too because
17423 // most PG clients default to it; SPG doesn't distinguish
17424 // the two (no path-operator perf advantage to model).
17425 "json" => ColumnTypeName::Json,
17426 "jsonb" => ColumnTypeName::Jsonb,
17427 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17428 // surface here. Same storage shape; mapping happens at
17429 // the engine side via the ColumnTypeName → DataType
17430 // resolver. Literal forms are handled at coerce_value
17431 // time so the lexer stays untouched.
17432 "bytea" | "bytes" => ColumnTypeName::Bytes,
17433 // v7.17.0 Phase 7 — PG network address types
17434 // v7.17.0 had a Text-backed fallback here for
17435 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17436 // each to a first-class type; the keywords are
17437 // bound below in the ζ-A block.
17438 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17439 // The actual `to_tsvector` / `@@` / `ts_rank` surface
17440 // arrives in v7.12.1+; the type itself loads here so
17441 // mailrs's `scripts/init-schema.sql` runs unmodified.
17442 "tsvector" => ColumnTypeName::TsVector,
17443 "tsquery" => ColumnTypeName::TsQuery,
17444 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17445 // surface for Django / Rails / Hibernate's default
17446 // PK pattern.
17447 "uuid" => ColumnTypeName::Uuid,
17448 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17449 // Storage = three-field {months, days, micros}, catalog
17450 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17451 // line `INTERVAL` was parser-rejected at CREATE TABLE.
17452 "interval" => {
17453 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17454 // SECOND` and an optional `(p)` precision. SPG stores the full
17455 // {months,days,micros}; consume + ignore the qualifier/precision.
17456 while matches!(self.peek(), Token::To)
17457 || matches!(self.peek(), Token::Ident(s) if matches!(
17458 s.to_ascii_lowercase().as_str(),
17459 "year" | "month" | "day" | "hour" | "minute" | "second"
17460 ))
17461 {
17462 self.advance();
17463 }
17464 self.consume_optional_paren_size();
17465 ColumnTypeName::Interval
17466 }
17467 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17468 // i64 microseconds since 00:00:00. Wire OID 1083.
17469 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17470 "time" => {
17471 // v7.39 (round 424) — MySQL TIME carries a semantic
17472 // fractional-seconds precision, bare meaning 0.
17473 let n = self.take_optional_paren_size();
17474 if self.mysql_dialect {
17475 mysql_fsp = Some(n.unwrap_or(0).min(6));
17476 }
17477 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17478 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17479 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17480 {
17481 self.advance();
17482 self.advance();
17483 self.advance();
17484 ColumnTypeName::TimeTz
17485 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17486 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17487 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17488 {
17489 self.advance();
17490 self.advance();
17491 self.advance();
17492 ColumnTypeName::Time
17493 } else {
17494 ColumnTypeName::Time
17495 }
17496 }
17497 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17498 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17499 "year" => ColumnTypeName::Year,
17500 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17501 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17502 "timetz" => ColumnTypeName::TimeTz,
17503 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17504 // Wire OID 790.
17505 "money" => ColumnTypeName::Money,
17506 // v7.17.0 Phase 3.P0-38 — PG range types.
17507 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17508 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17509 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17510 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17511 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17512 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17513 // v7.37.5 δ — PG 14+ multirange keywords.
17514 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17515 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17516 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17517 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17518 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17519 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17520 // v7.37.5 ε — PG geometry scalar keywords.
17521 "point" => ColumnTypeName::Point,
17522 "lseg" => ColumnTypeName::Lseg,
17523 "path" => ColumnTypeName::Path,
17524 "box" => ColumnTypeName::PgBox,
17525 "polygon" => ColumnTypeName::Polygon,
17526 "line" => ColumnTypeName::Line,
17527 "circle" => ColumnTypeName::Circle,
17528 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17529 "inet" => ColumnTypeName::Inet,
17530 "cidr" => ColumnTypeName::Cidr,
17531 "macaddr" => ColumnTypeName::Macaddr,
17532 "macaddr8" => ColumnTypeName::Macaddr8,
17533 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17534 // width in the value, so the optional `(N)` typmod is accepted and
17535 // ignored (the column stores whatever width it's given).
17536 "bit" => {
17537 let varying = matches!(
17538 self.peek(),
17539 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17540 );
17541 if varying {
17542 self.advance();
17543 }
17544 // v7.39 (round 281) — the length used to be parsed and
17545 // dropped, so `bit(3)` accepted a five-bit string.
17546 let n = if matches!(self.peek(), Token::LParen) {
17547 self.parse_paren_size("BIT")?
17548 } else {
17549 0
17550 };
17551 if varying {
17552 ColumnTypeName::BitVarying(n)
17553 } else {
17554 ColumnTypeName::Bit(n)
17555 }
17556 }
17557 "varbit" => {
17558 let n = if matches!(self.peek(), Token::LParen) {
17559 self.parse_paren_size("VARBIT")?
17560 } else {
17561 0
17562 };
17563 ColumnTypeName::BitVarying(n)
17564 }
17565 "xml" => ColumnTypeName::Xml,
17566 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17567 "hstore" => ColumnTypeName::Hstore,
17568 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17569 // `ENUM('a','b','c')`. Storage is TEXT; the value
17570 // list lands on `inline_enum_variants` for the
17571 // engine to validate INSERT cells against. Empty
17572 // value list is a parse error (matches MySQL).
17573 "enum" => {
17574 // Expect the opening `(`.
17575 if !matches!(self.peek(), Token::LParen) {
17576 return Err(self.err(alloc::format!(
17577 "expected '(' after ENUM, got {:?}",
17578 self.peek()
17579 )));
17580 }
17581 self.advance();
17582 let mut variants: Vec<String> = Vec::new();
17583 loop {
17584 match self.advance() {
17585 Token::String(s) => variants.push(s),
17586 other => {
17587 return Err(self.err(alloc::format!(
17588 "ENUM(...) expects string literal variants, got {other:?}"
17589 )));
17590 }
17591 }
17592 match self.peek() {
17593 Token::Comma => {
17594 self.advance();
17595 continue;
17596 }
17597 Token::RParen => {
17598 self.advance();
17599 break;
17600 }
17601 other => {
17602 return Err(self.err(alloc::format!(
17603 "expected ',' or ')' in ENUM(...), got {other:?}"
17604 )));
17605 }
17606 }
17607 }
17608 if variants.is_empty() {
17609 return Err(self.err("ENUM(...) must declare at least one variant".into()));
17610 }
17611 inline_enum_variants = Some(variants);
17612 // Storage is plain TEXT; the variant list lives on
17613 // the ColumnSchema side.
17614 ColumnTypeName::Text
17615 }
17616 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17617 // `SET('a','b','c')`. Same parse shape as ENUM;
17618 // semantics differ (subset rather than pick-one).
17619 "set" => {
17620 if !matches!(self.peek(), Token::LParen) {
17621 return Err(self.err(alloc::format!(
17622 "expected '(' after SET, got {:?}",
17623 self.peek()
17624 )));
17625 }
17626 self.advance();
17627 let mut variants: Vec<String> = Vec::new();
17628 loop {
17629 match self.advance() {
17630 Token::String(s) => variants.push(s),
17631 other => {
17632 return Err(self.err(alloc::format!(
17633 "SET(...) expects string literal variants, got {other:?}"
17634 )));
17635 }
17636 }
17637 match self.peek() {
17638 Token::Comma => {
17639 self.advance();
17640 continue;
17641 }
17642 Token::RParen => {
17643 self.advance();
17644 break;
17645 }
17646 other => {
17647 return Err(self.err(alloc::format!(
17648 "expected ',' or ')' in SET(...), got {other:?}"
17649 )));
17650 }
17651 }
17652 }
17653 if variants.is_empty() {
17654 return Err(self.err("SET(...) must declare at least one variant".into()));
17655 }
17656 inline_set_variants = Some(variants);
17657 ColumnTypeName::Text
17658 }
17659 _other => {
17660 // v7.17.0 Phase 1.4 — unknown ident → defer
17661 // resolution to the engine. Stored as Text in
17662 // ColumnTypeName + the original name carried as
17663 // `user_type_ref` so CREATE TABLE can look up
17664 // user-defined enum / domain types.
17665 user_type_ref = Some(ty_ident.clone());
17666 ColumnTypeName::Text
17667 }
17668 };
17669 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17670 // right after the type keyword. Pre-4.4 SPG consumed +
17671 // discarded the keyword, leaving a customer column
17672 // declared `id INT UNSIGNED NOT NULL` silently accepting
17673 // negative values — a Tier-A correctness drift where
17674 // application invariants (auto-increment-IDs never
17675 // negative) silently broke on cutover. Now: capture as
17676 // a column flag, persist on the schema, enforce at
17677 // INSERT / UPDATE time.
17678 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17679 {
17680 self.advance();
17681 true
17682 } else {
17683 false
17684 };
17685 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17686 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17687 // stores text as UTF-8 always so CHARACTER SET is still a
17688 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17689 // name: it gets classified into a `Collation` variant the
17690 // engine consults at WHERE-eval time. PG `default` /
17691 // `pg_catalog.default` / `C` / `POSIX` collations all
17692 // resolve to `Binary` (the prior behaviour); `_ci` /
17693 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17694 // The schema-qualifier form (`pg_catalog.default`) lexes
17695 // as `Ident '.' Ident` — peek for the `.` and consume both
17696 // halves so it's treated as one collation name. PG's
17697 // `IDENT.IDENT` collation form (which can appear here) is
17698 // resolved by Collation::from_collation_name on the bare
17699 // identifier after the dot.
17700 let mut collation = Collation::Binary;
17701 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17702 // clause was written. The engine needs this to tell an explicit
17703 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17704 // clause at all: both resolve to `Collation::Binary`, but under the
17705 // MySQL dialect the latter takes the folding default collation.
17706 let mut collation_explicit = false;
17707 let mut collation_name: Option<alloc::string::String> = None;
17708 loop {
17709 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17710 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17711 {
17712 self.advance(); // CHARACTER
17713 self.advance(); // SET
17714 if matches!(
17715 self.peek(),
17716 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17717 ) {
17718 self.advance();
17719 }
17720 continue;
17721 }
17722 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17723 self.advance(); // COLLATE
17724 // Accept Ident / QuotedIdent / String AND the
17725 // keyword-tokenised `Default` (PG `pg_catalog.default`
17726 // and bare `DEFAULT` collation names — `default` is a
17727 // reserved word so the lexer hands back Token::Default
17728 // not Token::Ident).
17729 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17730 match this.peek().clone() {
17731 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17732 this.advance();
17733 Some(s)
17734 }
17735 Token::Default => {
17736 this.advance();
17737 Some(alloc::string::String::from("default"))
17738 }
17739 _ => None,
17740 }
17741 };
17742 let raw = if let Some(head) = read_collation_atom(self) {
17743 // Schema-qualified PG form: `pg_catalog.default`.
17744 if matches!(self.peek(), Token::Dot) {
17745 self.advance();
17746 let tail = read_collation_atom(self).unwrap_or_default();
17747 alloc::format!("{head}.{tail}")
17748 } else {
17749 head
17750 }
17751 } else {
17752 alloc::string::String::new()
17753 };
17754 if !raw.is_empty() {
17755 collation_explicit = true;
17756 // v7.39 (round 676) — keep the name too. The enum below
17757 // folds C / POSIX / en_US / default into one value, and
17758 // `pg_attribute.attcollation` has to tell them apart.
17759 // The schema qualifier goes: PG's `pg_catalog.default`
17760 // and a bare `default` name the same collation.
17761 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17762 // encoding suffix. Round 676 used `rsplit('.')` for
17763 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17764 // PG writes `pg_catalog.default` (qualifier) and
17765 // `en_US.utf8` (locale + encoding) with the same
17766 // separator. Only `pg_catalog.` is a qualifier, and it
17767 // is the only one PG's own dumps emit.
17768 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17769 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17770 collation_name = Some(alloc::string::String::from(bare));
17771 let parsed = Collation::from_collation_name(&raw);
17772 // Last COLLATE clause wins, but `Binary` from a
17773 // bare keyword like `default` should not
17774 // silently downgrade a stronger one set earlier
17775 // on the same column. v7.17 only ships one
17776 // non-Binary variant so a simple OR is enough.
17777 if parsed != Collation::Binary {
17778 collation = parsed;
17779 }
17780 }
17781 continue;
17782 }
17783 break;
17784 }
17785 // v7.10.10 — postfix `[]` widens the base type to its array
17786 // type. PG accepts `TYPE[]` after any base type and so does
17787 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17788 // all through; the old "only TEXT[]" note was stale).
17789 if matches!(self.peek(), Token::LBracket) {
17790 self.advance();
17791 if !matches!(self.peek(), Token::RBracket) {
17792 return Err(self.err(alloc::format!(
17793 "TEXT[] takes no dimension; got {:?}",
17794 self.peek()
17795 )));
17796 }
17797 self.advance();
17798 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17799 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17800 // still error here.
17801 ty = match ty {
17802 ColumnTypeName::Text => ColumnTypeName::TextArray,
17803 ColumnTypeName::Int => ColumnTypeName::IntArray,
17804 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17805 // v7.40.0 — `oid[]`. Everything downstream of the
17806 // parser already handled `DataType::OidArray`; this
17807 // arm is the whole of what was missing.
17808 ColumnTypeName::Oid => ColumnTypeName::OidArray,
17809 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17810 // `[]` grammar. Wire OID 1187.
17811 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17812 // v7.37.5 γ — full PG array-of-scalar family.
17813 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17814 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17815 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17816 // NUMERIC(p, s) loses its precision params at the
17817 // array level (matches PG: `NUMERIC[]` is untyped,
17818 // per-element precision flows through values).
17819 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17820 ColumnTypeName::Date => ColumnTypeName::DateArray,
17821 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17822 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17823 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17824 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17825 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17826 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17827 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17828 // the array level (matches PG semantics where the
17829 // element precision is per-row, not column-wide).
17830 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17831 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17832 // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17833 // precision the same way NUMERIC[] does.
17834 ColumnTypeName::Real => ColumnTypeName::RealArray,
17835 ColumnTypeName::Time => ColumnTypeName::TimeArray,
17836 ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17837 ColumnTypeName::Inet => ColumnTypeName::InetArray,
17838 ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17839 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17840 // follow-up.
17841 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17842 other => {
17843 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17844 }
17845 };
17846 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17847 // for INT/TEXT/BIGINT. Anything else is an error.
17848 if matches!(self.peek(), Token::LBracket) {
17849 self.advance();
17850 if !matches!(self.peek(), Token::RBracket) {
17851 return Err(self.err(alloc::format!(
17852 "TYPE[][] second dimension takes no size; got {:?}",
17853 self.peek()
17854 )));
17855 }
17856 self.advance();
17857 ty = match ty {
17858 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17859 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17860 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17861 // v7.39 (read01 round 75) — bool[][].
17862 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17863 other => {
17864 return Err(self.err(alloc::format!(
17865 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17866 TEXT[][] only; got {other:?}"
17867 )));
17868 }
17869 };
17870 }
17871 }
17872 Ok((
17873 ty,
17874 implied_auto_increment,
17875 implied_not_null,
17876 user_type_ref,
17877 collation,
17878 collation_explicit,
17879 collation_name,
17880 is_unsigned,
17881 inline_enum_variants,
17882 inline_set_variants,
17883 mysql_int_width,
17884 mysql_fsp,
17885 mysql_declared_timestamp,
17886 mysql_float_md,
17887 ))
17888 }
17889
17890 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17891 // v7.20 — PG reserves the table-constraint keywords, so a
17892 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17893 // malformed constraint clause (e.g. `UNIQUE a` missing its
17894 // parens), not a column named "unique". Since v7.17's
17895 // unknown-type leniency (`user_type_ref`) such a clause
17896 // would otherwise parse as a column with a user-defined
17897 // type — silently accepting invalid DDL. Quoted
17898 // identifiers ("unique" / `unique`) remain valid names.
17899 if let Token::Ident(s) = self.peek()
17900 && [
17901 "unique",
17902 "primary",
17903 "foreign",
17904 "constraint",
17905 "check",
17906 "references",
17907 "exclude",
17908 ]
17909 .iter()
17910 .any(|kw| s.eq_ignore_ascii_case(kw))
17911 {
17912 return Err(self.err(alloc::format!(
17913 "unexpected reserved keyword '{s}' at start of column definition \
17914 (malformed table constraint?)"
17915 )));
17916 }
17917 let name_tok = self.pos;
17918 let name = self.expect_ident_like()?;
17919 // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17920 // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17921 // information_schema, and in SHOW CREATE (measured). SPG folded
17922 // an unquoted name, so a table restored from a dump reported
17923 // names the application had never written.
17924 //
17925 // The written form comes back from the source span, which only
17926 // the MySQL dialect keeps. The span runs to the START of the
17927 // next token, so a comment or unusual spacing between them
17928 // arrives with it — hence the check that what came back is the
17929 // same identifier. It is not decoration: without it,
17930 // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17931 // `MyCol /* c */`.
17932 let name = self
17933 .source_span(name_tok, name_tok)
17934 .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17935 .filter(|raw| raw.eq_ignore_ascii_case(&name))
17936 .map_or(name, alloc::string::String::from);
17937 let (
17938 ty,
17939 implied_auto_increment,
17940 implied_not_null,
17941 user_type_ref,
17942 collation,
17943 collation_explicit,
17944 collation_name,
17945 is_unsigned,
17946 inline_enum_variants,
17947 inline_set_variants,
17948 mysql_int_width,
17949 mysql_fsp,
17950 mysql_declared_timestamp,
17951 mysql_float_md,
17952 ) = self.parse_type_with_implied_flags()?;
17953 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17954 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17955 // each at most once.
17956 let mut default: Option<Expr> = None;
17957 let mut nullable = !implied_not_null;
17958 let mut nullability_seen = implied_not_null;
17959 let mut auto_increment = implied_auto_increment;
17960 let mut is_primary_key = false;
17961 let mut is_unique = false;
17962 let mut unique_nulls_not_distinct = false;
17963 let mut constraint_deferrable = false;
17964 let mut constraint_initially_deferred = false;
17965 let mut check: Option<Expr> = None;
17966 let mut on_update_runtime: Option<Expr> = None;
17967 let mut generated_stored_expr: Option<Box<Expr>> = None;
17968 let mut identity_always = false;
17969 loop {
17970 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17971 // not-null constraints by name and pg_dump emits them
17972 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17973 // NOT NULL`. Accept and discard the name; whatever
17974 // constraint follows is parsed by the arms below.
17975 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17976 // v7.39 (round 308, V29) — a name on an inline
17977 // REFERENCES belongs to the FOREIGN KEY, and the caller
17978 // (`parse_column_def_with_fk`) is what builds it, so
17979 // leave the whole clause for it. Dropping the name here
17980 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17981 // as the synthesised `c_pid_fkey` — which then could
17982 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17983 // `advance()` takes tokens by `mem::replace`, so there
17984 // is no rewinding once consumed.
17985 if matches!(
17986 self.tokens.get(self.pos + 2),
17987 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17988 ) {
17989 break;
17990 }
17991 self.advance();
17992 let _name = self.expect_ident_like()?;
17993 continue;
17994 }
17995 // v7.39 (round 379) — MySQL's SHORT generated-column form
17996 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17997 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17998 // below), but hand-written schemas and app migrations use this.
17999 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
18000 // SPG computes-and-stores either way, like the long form.
18001 if matches!(self.peek(), Token::As) {
18002 self.advance();
18003 if !matches!(self.peek(), Token::LParen) {
18004 return Err(self.err(alloc::format!(
18005 "expected '(' after AS in a generated column, got {:?}",
18006 self.peek()
18007 )));
18008 }
18009 self.advance();
18010 let expr = self.parse_expr(0)?;
18011 if !matches!(self.peek(), Token::RParen) {
18012 return Err(self.err(alloc::format!(
18013 "expected ')' after AS (<expr>), got {:?}",
18014 self.peek()
18015 )));
18016 }
18017 self.advance();
18018 if matches!(self.peek(), Token::Ident(s)
18019 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
18020 {
18021 self.advance();
18022 }
18023 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
18024 continue;
18025 }
18026 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
18027 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
18028 // the modern replacement for SERIAL in hand-written
18029 // schemas). Both flavours map onto the auto-increment
18030 // machinery — SPG's serial semantics ≈ BY DEFAULT;
18031 // ALWAYS's reject-explicit-values nuance is documented
18032 // leniency. Generated EXPRESSION columns
18033 // (`AS (expr) STORED`) are not supported: error loudly
18034 // instead of silently storing NULLs.
18035 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
18036 self.advance();
18037 let mut saw_generated_always = false;
18038 match self.peek().clone() {
18039 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
18040 self.advance();
18041 saw_generated_always = true;
18042 }
18043 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
18044 self.advance();
18045 if !matches!(self.peek(), Token::Default) {
18046 return Err(self.err(alloc::format!(
18047 "expected DEFAULT after GENERATED BY, got {:?}",
18048 self.peek()
18049 )));
18050 }
18051 self.advance();
18052 }
18053 other => {
18054 return Err(self.err(alloc::format!(
18055 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
18056 )));
18057 }
18058 }
18059 if !matches!(self.peek(), Token::As) {
18060 return Err(self.err(alloc::format!(
18061 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
18062 self.peek()
18063 )));
18064 }
18065 self.advance();
18066 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
18067 // ( <expr> ) STORED` stored computed-column. The
18068 // expression is captured for the engine to recompute
18069 // on every INSERT / UPDATE. v7.37.7 accepts the
18070 // STORED keyword only; PG also has VIRTUAL, which
18071 // v7.37.7 carves out (sentori only uses STORED).
18072 if matches!(self.peek(), Token::LParen) {
18073 self.advance();
18074 let expr = self.parse_expr(0)?;
18075 if !matches!(self.peek(), Token::RParen) {
18076 return Err(self.err(alloc::format!(
18077 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
18078 self.peek()
18079 )));
18080 }
18081 self.advance();
18082 let stored = match self.peek() {
18083 Token::Ident(s) | Token::QuotedIdent(s)
18084 if s.eq_ignore_ascii_case("stored") =>
18085 {
18086 self.advance();
18087 true
18088 }
18089 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
18090 // generated columns. SPG computes them on write and
18091 // persists like STORED; the two are observably
18092 // identical for query results (the value, recompute
18093 // on base-column change, and NOT NULL enforcement all
18094 // match), so a PG 18 schema/dump using VIRTUAL loads
18095 // and behaves correctly. The compute-on-read storage
18096 // saving is an invisible internal difference.
18097 Token::Ident(s) | Token::QuotedIdent(s)
18098 if s.eq_ignore_ascii_case("virtual") =>
18099 {
18100 self.advance();
18101 false
18102 }
18103 other => {
18104 return Err(self.err(alloc::format!(
18105 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18106 got {other:?}"
18107 )));
18108 }
18109 };
18110 let _ = stored; // STORED / VIRTUAL both compute-and-store.
18111 generated_stored_expr = Some(Box::new(expr));
18112 continue;
18113 }
18114 self.expect_keyword_ident("identity")?;
18115 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18116 // consume the balanced parens and discard (SPG's
18117 // auto-increment is max+1-scan based).
18118 if matches!(self.peek(), Token::LParen) {
18119 let mut depth = 0usize;
18120 loop {
18121 match self.advance() {
18122 Token::LParen => depth += 1,
18123 Token::RParen => {
18124 depth -= 1;
18125 if depth == 0 {
18126 break;
18127 }
18128 }
18129 Token::Eof => {
18130 return Err(self.err(
18131 "unterminated sequence-options parens after IDENTITY".into(),
18132 ));
18133 }
18134 _ => {}
18135 }
18136 }
18137 }
18138 auto_increment = true;
18139 // v7.38 (read01) — remember the ALWAYS flavour so the engine
18140 // can reject explicit non-DEFAULT INSERT values (unless
18141 // OVERRIDING SYSTEM VALUE) the way PG does.
18142 identity_always = saw_generated_always;
18143 // PG identity columns are implicitly NOT NULL.
18144 nullable = false;
18145 continue;
18146 }
18147 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18148 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18149 // is accepted today. The "ON" token is an Ident
18150 // (not reserved) — peek before consuming.
18151 if matches!(self.peek(), Token::On)
18152 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18153 {
18154 self.advance(); // ON
18155 self.advance(); // update
18156 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18157 let next = self.peek().clone();
18158 match next {
18159 Token::Ident(s) | Token::QuotedIdent(s)
18160 if s.eq_ignore_ascii_case("current_timestamp") =>
18161 {
18162 self.advance();
18163 // Optional `(N)` precision.
18164 if matches!(self.peek(), Token::LParen) {
18165 self.advance();
18166 if !matches!(self.peek(), Token::Integer(_)) {
18167 return Err(self.err(alloc::format!(
18168 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18169 self.peek()
18170 )));
18171 }
18172 self.advance();
18173 if !matches!(self.peek(), Token::RParen) {
18174 return Err(self.err(alloc::format!(
18175 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18176 self.peek()
18177 )));
18178 }
18179 self.advance();
18180 }
18181 on_update_runtime = Some(Expr::FunctionCall {
18182 name: "now".into(),
18183 args: Vec::new(),
18184 });
18185 continue;
18186 }
18187 other => {
18188 return Err(self.err(alloc::format!(
18189 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18190 )));
18191 }
18192 }
18193 }
18194 if matches!(self.peek(), Token::Default) {
18195 if default.is_some() {
18196 return Err(self.err("DEFAULT specified twice".into()));
18197 }
18198 self.advance();
18199 default = Some(self.parse_expr(0)?);
18200 continue;
18201 }
18202 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18203 // token with NOT NULL and sits EARLIER in the loop than the
18204 // deferrability arm, so without the lookahead it was reported as
18205 // "NOT NULL specified twice" (or "expected NULL after NOT").
18206 if matches!(self.peek(), Token::Not)
18207 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18208 {
18209 // NOT DEFERRABLE — explicit immediate; nothing to carry.
18210 self.consume_optional_deferrable_clauses()?;
18211 continue;
18212 }
18213 if matches!(self.peek(), Token::Not) {
18214 if nullability_seen {
18215 return Err(self.err("NOT NULL specified twice".into()));
18216 }
18217 self.advance();
18218 if !matches!(self.peek(), Token::Null) {
18219 return Err(self.err(format!(
18220 "expected NULL after NOT in column def, got {:?}",
18221 self.peek()
18222 )));
18223 }
18224 self.advance();
18225 nullable = false;
18226 nullability_seen = true;
18227 continue;
18228 }
18229 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18230 // "this column is nullable" marker (the default in
18231 // standard SQL anyway). mysqldump emits it routinely
18232 // (`col TYPE NULL DEFAULT NULL` for nullable
18233 // timestamps etc). Accept + no-op.
18234 if matches!(self.peek(), Token::Null) {
18235 if nullability_seen && !nullable {
18236 // v7.39 (round 761, F31 tranche 2 #31) — PG's
18237 // sentence, PG18-measured (the table name is the
18238 // caller's; the column half is exact).
18239 return Err(self.err(alloc::format!(
18240 "conflicting NULL/NOT NULL declarations for column \"{name}\""
18241 )));
18242 }
18243 self.advance();
18244 nullable = true;
18245 nullability_seen = true;
18246 continue;
18247 }
18248 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18249 // arrives as a bare Ident. Match either, case-insensitive.
18250 if let Token::Ident(s) = self.peek()
18251 && (s.eq_ignore_ascii_case("auto_increment")
18252 || s.eq_ignore_ascii_case("autoincrement"))
18253 {
18254 if auto_increment {
18255 return Err(self.err("AUTO_INCREMENT specified twice".into()));
18256 }
18257 self.advance();
18258 auto_increment = true;
18259 continue;
18260 }
18261 // v7.9.13 — inline `PRIMARY KEY` column constraint
18262 // (mailrs F1). Implies `NOT NULL`. The engine creates
18263 // a BTree index for the PK column at CREATE TABLE time
18264 // so FK parent-side index lookups resolve.
18265 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18266 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18267 // spelling was a parse error, so a pg_dump carrying one stopped
18268 // mid-restore. The clauses are consumed by the same helper the FK
18269 // path has used since round 288 and recorded nowhere: SPG enforces
18270 // the constraint IMMEDIATELY either way, which fails earlier than
18271 // PG inside a transaction that violates-then-repairs — a refusal,
18272 // not a wrong answer. True deferral is the open remainder of F08.
18273 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18274 || (matches!(self.peek(), Token::Not)
18275 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18276 {
18277 // v7.39 (round 711) — CARRIED now (the storing half of
18278 // F08); round 621 only consumed.
18279 let (d, idef) = self.consume_deferrable_clauses_timed()?;
18280 constraint_deferrable |= d;
18281 constraint_initially_deferred |= idef;
18282 continue;
18283 }
18284 if let Token::Ident(s) = self.peek()
18285 && s.eq_ignore_ascii_case("primary")
18286 {
18287 if is_primary_key {
18288 return Err(self.err("PRIMARY KEY specified twice".into()));
18289 }
18290 // Peek-ahead for the required `KEY` token.
18291 let next = self.tokens.get(self.pos + 1);
18292 let next_is_key = matches!(
18293 next,
18294 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18295 );
18296 if !next_is_key {
18297 return Err(self.err(format!(
18298 "expected KEY after PRIMARY in column def, got {:?}",
18299 next
18300 )));
18301 }
18302 self.advance(); // PRIMARY
18303 self.advance(); // KEY
18304 is_primary_key = true;
18305 if nullability_seen && nullable {
18306 return Err(self.err(
18307 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18308 ));
18309 }
18310 nullable = false;
18311 nullability_seen = true;
18312 continue;
18313 }
18314 // v7.13.0 — inline `UNIQUE` column constraint
18315 // (mailrs round-5 G2). Fold into a single-column
18316 // table-level UNIQUE at CREATE TABLE post-process time.
18317 if let Token::Ident(s) = self.peek()
18318 && s.eq_ignore_ascii_case("unique")
18319 {
18320 if is_unique {
18321 return Err(self.err("UNIQUE specified twice".into()));
18322 }
18323 self.advance();
18324 is_unique = true;
18325 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18326 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18327 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18328 let n1 = self.tokens.get(self.pos + 1);
18329 let n2 = self.tokens.get(self.pos + 2);
18330 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18331 self.advance(); // NULLS
18332 self.advance(); // NOT
18333 self.advance(); // DISTINCT
18334 unique_nulls_not_distinct = true;
18335 } else if matches!(n1, Some(Token::Distinct)) {
18336 self.advance(); // NULLS
18337 self.advance(); // DISTINCT
18338 }
18339 }
18340 continue;
18341 }
18342 // v7.13.0 — inline `CHECK (<expr>)` column constraint
18343 // (mailrs round-5 G3). PG semantics: column-level
18344 // CHECK is equivalent to a table-level CHECK. Multiple
18345 // inline CHECKs on the same column AND together.
18346 if let Token::Ident(s) = self.peek()
18347 && s.eq_ignore_ascii_case("check")
18348 {
18349 self.advance();
18350 if !matches!(self.peek(), Token::LParen) {
18351 return Err(self.err(alloc::format!(
18352 "expected '(' after CHECK in column def, got {:?}",
18353 self.peek()
18354 )));
18355 }
18356 self.advance();
18357 let pred = self.parse_expr(0)?;
18358 if !matches!(self.peek(), Token::RParen) {
18359 return Err(self.err(alloc::format!(
18360 "expected ')' to close CHECK predicate, got {:?}",
18361 self.peek()
18362 )));
18363 }
18364 self.advance();
18365 check = Some(match check.take() {
18366 Some(prev) => Expr::Binary {
18367 op: BinOp::And,
18368 lhs: Box::new(prev),
18369 rhs: Box::new(pred),
18370 },
18371 None => pred,
18372 });
18373 continue;
18374 }
18375 break;
18376 }
18377 Ok(ColumnDef {
18378 name,
18379 ty,
18380 nullable,
18381 default,
18382 auto_increment,
18383 is_primary_key,
18384 is_unique,
18385 unique_nulls_not_distinct,
18386 constraint_deferrable,
18387 constraint_initially_deferred,
18388 check,
18389 user_type_ref,
18390 on_update_runtime,
18391 collation,
18392 collation_explicit,
18393 collation_name,
18394 is_unsigned,
18395 inline_enum_variants,
18396 inline_set_variants,
18397 generated_stored_expr,
18398 identity_always,
18399 mysql_int_width,
18400 mysql_fsp,
18401 mysql_declared_timestamp,
18402 mysql_float_md,
18403 })
18404 }
18405
18406 /// `NUMERIC` may appear without parameters, with one (precision
18407 /// only, scale=0), or with both. Returns `(precision, scale)` with
18408 /// 0 = unspecified for the bare form.
18409 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18410 if !matches!(self.peek(), Token::LParen) {
18411 // Bare `NUMERIC` — PG treats this as "unlimited precision";
18412 // we surface it as precision=0 to mean "unconstrained" so
18413 // the engine doesn't need a separate variant.
18414 return Ok((0, 0));
18415 }
18416 self.advance();
18417 // v7.39 (round 272) — PG's declared precision runs to 1000, and
18418 // it words the out-of-range case with the value it saw. SPG
18419 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18420 // accepts failed to parse at all; values wider than i128 are
18421 // carried by the arbitrary-precision form.
18422 let precision = match self.advance() {
18423 Token::Integer(n) if (1..=1000).contains(&n) => {
18424 u16::try_from(n).expect("range-checked")
18425 }
18426 Token::Integer(n) => {
18427 return Err(ParseError {
18428 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18429 token_pos: self.consumed_pos(),
18430 });
18431 }
18432 other => {
18433 return Err(ParseError {
18434 message: format!(
18435 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18436 ),
18437 token_pos: self.consumed_pos(),
18438 });
18439 }
18440 };
18441 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18442 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18443 // then overflows). A negative scale rounds to tens / hundreds / …
18444 let scale = if matches!(self.peek(), Token::Comma) {
18445 self.advance();
18446 let neg = if matches!(self.peek(), Token::Minus) {
18447 self.advance();
18448 true
18449 } else {
18450 false
18451 };
18452 match self.advance() {
18453 Token::Integer(n) => {
18454 let signed = if neg { -n } else { n };
18455 if !(-1000..=1000).contains(&signed) {
18456 return Err(ParseError {
18457 message: format!(
18458 "NUMERIC scale {signed} must be between -1000 and 1000"
18459 ),
18460 token_pos: self.consumed_pos(),
18461 });
18462 }
18463 i16::try_from(signed).expect("range-checked")
18464 }
18465 other => {
18466 return Err(ParseError {
18467 message: format!("NUMERIC scale must be an integer, got {other:?}"),
18468 token_pos: self.consumed_pos(),
18469 });
18470 }
18471 }
18472 } else {
18473 0
18474 };
18475 if !matches!(self.peek(), Token::RParen) {
18476 return Err(self.err(format!(
18477 "expected ')' to close NUMERIC params, got {:?}",
18478 self.peek()
18479 )));
18480 }
18481 self.advance();
18482 Ok((precision, scale))
18483 }
18484
18485 /// Parse `(N)` where `N` is a positive integer literal — used by the
18486 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18487 /// for the error message.
18488 /// v6.0.1: parse the optional `USING <encoding>` clause that
18489 /// follows `VECTOR(N)` in a column definition. Missing clause
18490 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18491 /// ident → `ParseError` listing the encodings recognised today.
18492 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18493 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18494 return Ok(VecEncoding::F32);
18495 }
18496 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18497 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18498 // consume the token when the very next token is a known
18499 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18500 // `USING` for the caller — it's the rewrite-expression form.
18501 let n1 = self.tokens.get(self.pos + 1);
18502 let next_is_encoding = matches!(
18503 n1,
18504 Some(Token::Ident(s))
18505 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18506 );
18507 if !next_is_encoding {
18508 return Ok(VecEncoding::F32);
18509 }
18510 self.advance();
18511 let enc_ident = match self.advance() {
18512 Token::Ident(s) => s,
18513 other => {
18514 return Err(self.err(format!(
18515 "expected vector encoding after USING, got {other:?}"
18516 )));
18517 }
18518 };
18519 match enc_ident.to_ascii_lowercase().as_str() {
18520 "sq8" => Ok(VecEncoding::Sq8),
18521 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18522 // binary16 per-element storage.
18523 "half" => Ok(VecEncoding::F16),
18524 other => Err(self.err(format!(
18525 "unknown vector encoding {other:?}; supported: SQ8, HALF"
18526 ))),
18527 }
18528 }
18529
18530 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18531 /// without consuming it. Returns `Some(N)` when the next
18532 /// tokens are `( <int> )`; None otherwise. Used by the
18533 /// TINYINT classifier to decide whether to map to Bool or
18534 /// SmallInt.
18535 fn peek_optional_paren_size_value(&self) -> Option<i64> {
18536 if !matches!(self.peek(), Token::LParen) {
18537 return None;
18538 }
18539 let next = self.tokens.get(self.pos + 1)?;
18540 let n = match next {
18541 Token::Integer(n) => *n,
18542 _ => return None,
18543 };
18544 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18545 return None;
18546 }
18547 Some(n)
18548 }
18549
18550 /// v7.14.0 — consume an optional MySQL display-width
18551 /// parenthesised number after an integer type, returning
18552 /// nothing. `TINYINT(1)` etc.
18553 /// v7.39 (round 360) — does the parenthesised group ahead contain a
18554 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18555 fn peek_paren_has_comma(&self) -> bool {
18556 let mut i = self.pos + 1;
18557 let mut depth = 1usize;
18558 while depth > 0 {
18559 match self.tokens.get(i) {
18560 Some(Token::LParen) => depth += 1,
18561 Some(Token::RParen) => depth -= 1,
18562 Some(Token::Comma) if depth == 1 => return true,
18563 None | Some(Token::Eof) => return false,
18564 _ => {}
18565 }
18566 i += 1;
18567 }
18568 false
18569 }
18570
18571 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18572 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18573 /// fractional-seconds precision that drives write truncation and render
18574 /// padding, where `consume_optional_paren_size` throws it away.
18575 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18576 fn take_optional_paren_size(&mut self) -> Option<u8> {
18577 let Some(Token::Integer(n)) = self
18578 .tokens
18579 .get(self.pos + 1)
18580 .filter(|_| matches!(self.peek(), Token::LParen))
18581 .cloned()
18582 else {
18583 self.consume_optional_paren_size();
18584 return None;
18585 };
18586 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18587 self.consume_optional_paren_size();
18588 return None;
18589 }
18590 self.consume_optional_paren_size();
18591 u8::try_from(n).ok()
18592 }
18593
18594 fn consume_optional_paren_size(&mut self) {
18595 if !matches!(self.peek(), Token::LParen) {
18596 return;
18597 }
18598 self.advance();
18599 // Skip until matching RParen (allow nested or any tokens).
18600 let mut depth = 1usize;
18601 while depth > 0 {
18602 match self.peek() {
18603 Token::LParen => depth += 1,
18604 Token::RParen => depth -= 1,
18605 Token::Eof => return,
18606 _ => {}
18607 }
18608 self.advance();
18609 }
18610 }
18611
18612 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18613 if !matches!(self.peek(), Token::LParen) {
18614 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18615 }
18616 self.advance();
18617 let n = match self.advance() {
18618 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18619 message: format!("{label} size too large: {n}"),
18620 token_pos: self.consumed_pos(),
18621 })?,
18622 other => {
18623 return Err(ParseError {
18624 message: format!("expected positive integer {label} size, got {other:?}"),
18625 token_pos: self.consumed_pos(),
18626 });
18627 }
18628 };
18629 if !matches!(self.peek(), Token::RParen) {
18630 return Err(self.err(format!(
18631 "expected ')' after {label} size, got {:?}",
18632 self.peek()
18633 )));
18634 }
18635 self.advance();
18636 Ok(n)
18637 }
18638
18639 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18640 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18641 /// key, like MySQL) whose action skips conflicting rows.
18642 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18643 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18644 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18645 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18646 /// common bulk-upsert spellings —
18647 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18648 /// REPLACE INTO t SELECT …
18649 /// — were a parse error / a duplicate-key failure respectively.
18650 ///
18651 /// Precedence: an explicitly written clause beats a statement-level flag.
18652 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18653 /// implicit `REPLACE` and `IGNORE` lowerings.
18654 fn parse_insert_conflict_clause(
18655 &mut self,
18656 replace: bool,
18657 ignore: bool,
18658 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18659 if let Some(c) = self.parse_optional_on_duplicate_key()? {
18660 return Ok(Some(c));
18661 }
18662 if let Some(c) = self.parse_optional_on_conflict()? {
18663 return Ok(Some(c));
18664 }
18665 if replace {
18666 // REPLACE INTO = delete-then-insert, which PG spells as
18667 // `ON CONFLICT DO UPDATE SET` over every column; the engine
18668 // reads an empty assignment list as "take the incoming row".
18669 return Ok(Some(crate::ast::OnConflictClause {
18670 target_columns: Vec::new(),
18671 index_where: None,
18672 constraint_name: None,
18673 mysql_lowered: true,
18674 action: crate::ast::OnConflictAction::Update {
18675 assignments: Vec::new(),
18676 where_: None,
18677 },
18678 }));
18679 }
18680 if ignore {
18681 return Ok(Some(Self::insert_ignore_clause()));
18682 }
18683 Ok(None)
18684 }
18685
18686 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18687 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18688 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18689 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18690 fn parse_optional_on_duplicate_key(
18691 &mut self,
18692 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18693 if !(matches!(self.peek(), Token::On)
18694 && matches!(self.tokens.get(self.pos + 1),
18695 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18696 {
18697 return Ok(None);
18698 }
18699 self.advance(); // ON
18700 self.advance(); // DUPLICATE
18701 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18702 return Err(self.err(format!(
18703 "expected KEY after ON DUPLICATE, got {:?}",
18704 self.peek()
18705 )));
18706 }
18707 self.advance();
18708 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18709 return Err(self.err(format!(
18710 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18711 self.peek()
18712 )));
18713 }
18714 self.advance();
18715 let mut assignments: Vec<(String, Expr)> = Vec::new();
18716 loop {
18717 let col = self.expect_ident_like()?;
18718 if !matches!(self.peek(), Token::Eq) {
18719 return Err(self.err(format!(
18720 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18721 self.peek()
18722 )));
18723 }
18724 self.advance();
18725 let mut expr = self.parse_expr(0)?;
18726 Self::rewrite_mysql_values_refs(&mut expr);
18727 assignments.push((col, expr));
18728 if matches!(self.peek(), Token::Comma) {
18729 self.advance();
18730 continue;
18731 }
18732 break;
18733 }
18734 Ok(Some(crate::ast::OnConflictClause {
18735 target_columns: Vec::new(),
18736 index_where: None,
18737 constraint_name: None,
18738 mysql_lowered: true,
18739 action: crate::ast::OnConflictAction::Update {
18740 assignments,
18741 where_: None,
18742 },
18743 }))
18744 }
18745
18746 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18747 crate::ast::OnConflictClause {
18748 target_columns: Vec::new(),
18749 index_where: None,
18750 constraint_name: None,
18751 mysql_lowered: true,
18752 action: crate::ast::OnConflictAction::Nothing,
18753 }
18754 }
18755
18756 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18757 debug_assert!(
18758 matches!(self.peek(), Token::Insert)
18759 || (replace
18760 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18761 );
18762 self.advance();
18763 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18764 // would raise a duplicate-key error instead of failing the statement,
18765 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18766 // plain ident to the lexer; only the MySQL dialect accepts it here.
18767 let ignore = self.mysql_dialect
18768 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18769 if ignore {
18770 self.advance();
18771 }
18772 if !matches!(self.peek(), Token::Into) {
18773 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18774 }
18775 self.advance();
18776 let table = self.expect_ident_like()?;
18777 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18778 // grammar requires the AS keyword here (a bare identifier would be
18779 // ambiguous with a column list). The alias is what the ON CONFLICT
18780 // DO UPDATE expressions refer to the target row by.
18781 let alias = if matches!(self.peek(), Token::As) {
18782 self.advance();
18783 Some(self.expect_ident_like()?)
18784 } else {
18785 None
18786 };
18787 // v7.39 (round 428) — MySQL's SET-form INSERT:
18788 // INSERT INTO t SET a = 1, b = 'x'
18789 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18790 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18791 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18792 // measured). So it lowers to the column list + one VALUES row and
18793 // rejoins the ordinary path, which already handles every one of
18794 // those. PG has no such spelling, hence the dialect gate.
18795 if self.mysql_dialect
18796 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18797 {
18798 self.advance(); // SET
18799 let mut names = Vec::new();
18800 let mut values = Vec::new();
18801 loop {
18802 names.push(self.expect_ident_like()?);
18803 if !matches!(self.peek(), Token::Eq) {
18804 return Err(self.err(alloc::format!(
18805 "expected '=' in INSERT … SET, got {:?}",
18806 self.peek()
18807 )));
18808 }
18809 self.advance();
18810 // `SET a = DEFAULT` rides the same `__column_default` marker
18811 // the VALUES-row and UPDATE-SET paths use; the INSERT
18812 // executor resolves it against the target column.
18813 if matches!(self.peek(), Token::Default) {
18814 self.advance();
18815 values.push(Expr::FunctionCall {
18816 name: "__column_default".to_string(),
18817 args: Vec::new(),
18818 });
18819 } else {
18820 values.push(self.parse_expr(0)?);
18821 }
18822 if matches!(self.peek(), Token::Comma) {
18823 self.advance();
18824 continue;
18825 }
18826 break;
18827 }
18828 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18829 let returning = self.parse_optional_returning()?;
18830 return Ok(Statement::Insert(InsertStatement {
18831 ctes: Vec::new(),
18832 table,
18833 alias,
18834 columns: Some(names),
18835 rows: alloc::vec![values],
18836 select_source: None,
18837 // MySQL's SET form has no `OVERRIDING …` clause (that is
18838 // PG's identity-column spelling).
18839 overriding: Overriding::None,
18840 mysql_ignore: ignore,
18841 on_conflict,
18842 returning,
18843 }));
18844 }
18845 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18846 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18847 // a parenthesized query source instead (PG select_with_parens:
18848 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18849 // both keywords are reserved in PG, so no column list can start
18850 // with them.
18851 let columns = if matches!(self.peek(), Token::LParen) {
18852 self.advance();
18853 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18854 let select_stmt = if self.peek_is_with_kw() {
18855 self.advance();
18856 self.parse_nested_with_select()?
18857 } else {
18858 match self.parse_select_stmt()? {
18859 Statement::Select(s) => s,
18860 other => {
18861 return Err(self.err(alloc::format!(
18862 "expected SELECT in parenthesized INSERT source, got {other:?}"
18863 )));
18864 }
18865 }
18866 };
18867 if !matches!(self.peek(), Token::RParen) {
18868 return Err(self.err(format!(
18869 "expected ')' after parenthesized INSERT source, got {:?}",
18870 self.peek()
18871 )));
18872 }
18873 self.advance();
18874 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18875 let returning = self.parse_optional_returning()?;
18876 return Ok(Statement::Insert(InsertStatement {
18877 ctes: Vec::new(),
18878 table,
18879 alias: alias.clone(),
18880 columns: None,
18881 rows: Vec::new(),
18882 select_source: Some(Box::new(select_stmt)),
18883 on_conflict,
18884 returning,
18885 overriding: Overriding::None,
18886 mysql_ignore: ignore,
18887 }));
18888 }
18889 let mut names = Vec::new();
18890 loop {
18891 names.push(self.expect_ident_like()?);
18892 match self.peek() {
18893 Token::Comma => {
18894 self.advance();
18895 }
18896 Token::RParen => {
18897 self.advance();
18898 break;
18899 }
18900 other => {
18901 return Err(self.err(format!(
18902 "expected ',' or ')' in INSERT column list, got {other:?}"
18903 )));
18904 }
18905 }
18906 }
18907 Some(names)
18908 } else {
18909 None
18910 };
18911 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18912 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18913 // is captured on the statement so the engine can apply PG's
18914 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18915 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18916 {
18917 self.advance();
18918 let which = self.expect_ident_like()?;
18919 let ov = if which.eq_ignore_ascii_case("system") {
18920 Overriding::System
18921 } else if which.eq_ignore_ascii_case("user") {
18922 Overriding::User
18923 } else {
18924 return Err(self.err(format!(
18925 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18926 )));
18927 };
18928 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18929 return Err(self.err(format!(
18930 "expected VALUE after OVERRIDING {}, got {:?}",
18931 which.to_ascii_uppercase(),
18932 self.peek()
18933 )));
18934 }
18935 self.advance();
18936 ov
18937 } else {
18938 Overriding::None
18939 };
18940 // `INSERT INTO t DEFAULT VALUES` — a single row made
18941 // entirely of column defaults. Lower to the permuted
18942 // column-list path with an empty list: every schema column
18943 // is unmapped, so the engine fills each from its default
18944 // (serials advance, plain defaults evaluate, the rest NULL).
18945 if matches!(self.peek(), Token::Default) {
18946 self.advance();
18947 if !matches!(self.peek(), Token::Values) {
18948 return Err(self.err(format!(
18949 "expected VALUES after DEFAULT in INSERT, got {:?}",
18950 self.peek()
18951 )));
18952 }
18953 self.advance();
18954 if columns.is_some() {
18955 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18956 }
18957 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18958 let returning = self.parse_optional_returning()?;
18959 return Ok(Statement::Insert(InsertStatement {
18960 ctes: Vec::new(),
18961 table,
18962 alias: alias.clone(),
18963 columns: Some(Vec::new()),
18964 rows: alloc::vec![Vec::new()],
18965 select_source: None,
18966 on_conflict,
18967 returning,
18968 overriding,
18969 mysql_ignore: ignore,
18970 }));
18971 }
18972 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18973 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18974 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18975 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18976 // own WITH comes before INSERT).
18977 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18978 let select_stmt = if self.peek_is_with_kw() {
18979 self.advance();
18980 self.parse_nested_with_select()?
18981 } else {
18982 match self.parse_select_stmt()? {
18983 Statement::Select(s) => s,
18984 other => {
18985 return Err(self.err(alloc::format!(
18986 "expected SELECT after INSERT INTO ... target, got {other:?}"
18987 )));
18988 }
18989 }
18990 };
18991 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18992 let returning = self.parse_optional_returning()?;
18993 return Ok(Statement::Insert(InsertStatement {
18994 ctes: Vec::new(),
18995 table,
18996 alias: alias.clone(),
18997 columns,
18998 rows: Vec::new(),
18999 select_source: Some(Box::new(select_stmt)),
19000 on_conflict,
19001 returning,
19002 overriding,
19003 mysql_ignore: ignore,
19004 }));
19005 }
19006 if !matches!(self.peek(), Token::Values) {
19007 return Err(self.err(format!(
19008 "expected VALUES or SELECT after table name, got {:?}",
19009 self.peek()
19010 )));
19011 }
19012 self.advance();
19013 if !matches!(self.peek(), Token::LParen) {
19014 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
19015 }
19016 let mut rows = Vec::new();
19017 loop {
19018 // Each iteration consumes one `(expr, expr, …)` tuple.
19019 if !matches!(self.peek(), Token::LParen) {
19020 return Err(self.err(format!(
19021 "expected '(' for next VALUES tuple, got {:?}",
19022 self.peek()
19023 )));
19024 }
19025 self.advance();
19026 let mut tuple = Vec::new();
19027 loop {
19028 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
19029 // the column's declared default for that slot. Rides out as the
19030 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
19031 // path uses; the INSERT executor resolves it per target column.
19032 if matches!(self.peek(), Token::Default) {
19033 self.advance();
19034 tuple.push(Expr::FunctionCall {
19035 name: "__column_default".to_string(),
19036 args: Vec::new(),
19037 });
19038 } else {
19039 tuple.push(self.parse_expr(0)?);
19040 }
19041 match self.peek() {
19042 Token::Comma => {
19043 self.advance();
19044 }
19045 Token::RParen => {
19046 self.advance();
19047 break;
19048 }
19049 other => {
19050 return Err(self.err(format!(
19051 "expected ',' or ')' in VALUES tuple, got {other:?}"
19052 )));
19053 }
19054 }
19055 }
19056 if tuple.is_empty() {
19057 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
19058 }
19059 rows.push(tuple);
19060 // Continue with comma-separated tuples.
19061 if matches!(self.peek(), Token::Comma) {
19062 self.advance();
19063 } else {
19064 break;
19065 }
19066 }
19067 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
19068 // to ON CONFLICT DO UPDATE with an empty conflict target
19069 // (the engine picks the table's first unique index, which
19070 // matches MySQL's any-unique-key behaviour for the common
19071 // single-key case). `VALUES(col)` in the assignments is
19072 // MySQL's spelling of EXCLUDED.col.
19073 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
19074 let returning = self.parse_optional_returning()?;
19075 Ok(Statement::Insert(InsertStatement {
19076 ctes: Vec::new(),
19077 table,
19078 alias,
19079 columns,
19080 rows,
19081 select_source: None,
19082 on_conflict,
19083 returning,
19084 overriding,
19085 mysql_ignore: ignore,
19086 }))
19087 }
19088
19089 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
19090 /// the incoming row's value — exactly PG's EXCLUDED.col.
19091 fn rewrite_mysql_values_refs(e: &mut Expr) {
19092 match e {
19093 Expr::FunctionCall { name, args }
19094 if name.eq_ignore_ascii_case("values")
19095 && args.len() == 1
19096 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19097 {
19098 let Expr::Column(c) = &args[0] else {
19099 unreachable!("guarded above");
19100 };
19101 *e = Expr::Column(crate::ast::ColumnName {
19102 qualifier: Some("EXCLUDED".to_string()),
19103 name: c.name.clone(),
19104 });
19105 }
19106 Expr::FunctionCall { args, .. } => {
19107 for a in args {
19108 Self::rewrite_mysql_values_refs(a);
19109 }
19110 }
19111 Expr::Binary { lhs, rhs, .. } => {
19112 Self::rewrite_mysql_values_refs(lhs);
19113 Self::rewrite_mysql_values_refs(rhs);
19114 }
19115 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19116 Self::rewrite_mysql_values_refs(expr);
19117 }
19118 Expr::Case {
19119 operand,
19120 branches,
19121 else_branch,
19122 } => {
19123 if let Some(op) = operand {
19124 Self::rewrite_mysql_values_refs(op);
19125 }
19126 for (w, t) in branches {
19127 Self::rewrite_mysql_values_refs(w);
19128 Self::rewrite_mysql_values_refs(t);
19129 }
19130 if let Some(el) = else_branch {
19131 Self::rewrite_mysql_values_refs(el);
19132 }
19133 }
19134 _ => {}
19135 }
19136 }
19137
19138 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19139 /// clause sitting between the INSERT body and the trailing
19140 /// RETURNING. All keywords come in as bare idents; `ON` is
19141 /// a reserved Token though.
19142 fn parse_optional_on_conflict(
19143 &mut self,
19144 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19145 if !matches!(self.peek(), Token::On) {
19146 return Ok(None);
19147 }
19148 // Peek further: we want exactly "ON CONFLICT ...". If the
19149 // next ident isn't "conflict", let some other parser handle.
19150 let next_is_conflict = matches!(
19151 self.tokens.get(self.pos + 1),
19152 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19153 );
19154 if !next_is_conflict {
19155 return Ok(None);
19156 }
19157 self.advance(); // ON
19158 self.advance(); // CONFLICT
19159 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19160 // the constraint instead of listing columns (the pg_dump
19161 // form); the engine resolves it.
19162 let mut constraint_name: Option<String> = None;
19163 if matches!(self.peek(), Token::On) {
19164 self.advance(); // ON
19165 match self.advance() {
19166 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19167 }
19168 other => {
19169 return Err(self.err(alloc::format!(
19170 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19171 )));
19172 }
19173 }
19174 constraint_name = Some(self.expect_ident_like()?);
19175 }
19176 // Optional `(col [, col]*)` target list.
19177 let mut target_columns: Vec<String> = Vec::new();
19178 if matches!(self.peek(), Token::LParen) {
19179 self.advance();
19180 loop {
19181 target_columns.push(self.expect_ident_like()?);
19182 match self.peek() {
19183 Token::Comma => {
19184 self.advance();
19185 }
19186 Token::RParen => {
19187 self.advance();
19188 break;
19189 }
19190 other => {
19191 return Err(self.err(alloc::format!(
19192 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19193 )));
19194 }
19195 }
19196 }
19197 }
19198 // v7.39 (round 240) — optional index predicate after the target
19199 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19200 // PARTIAL unique index; SPG's arbiters are full indexes, which
19201 // satisfy any predicate, so it is parsed and carried but not
19202 // consulted (recorded residual: partial-unique-index arbiters).
19203 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19204 self.advance();
19205 Some(self.parse_expr(0)?)
19206 } else {
19207 None
19208 };
19209 // Required `DO`.
19210 match self.advance() {
19211 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19212 other => {
19213 return Err(self.err(alloc::format!(
19214 "expected DO after ON CONFLICT [(…)], got {other:?}"
19215 )));
19216 }
19217 }
19218 // Action: NOTHING | UPDATE SET …
19219 let action = match self.advance() {
19220 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19221 crate::ast::OnConflictAction::Nothing
19222 }
19223 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19224 self.parse_on_conflict_update_action()?
19225 }
19226 other => {
19227 return Err(self.err(alloc::format!(
19228 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19229 )));
19230 }
19231 };
19232 Ok(Some(crate::ast::OnConflictClause {
19233 target_columns,
19234 index_where,
19235 constraint_name,
19236 mysql_lowered: false,
19237 action,
19238 }))
19239 }
19240
19241 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19242 /// `SET col = expr [, …] [WHERE cond]`. Caller already
19243 /// consumed `UPDATE`.
19244 fn parse_on_conflict_update_action(
19245 &mut self,
19246 ) -> Result<crate::ast::OnConflictAction, ParseError> {
19247 // `SET`
19248 match self.advance() {
19249 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19250 other => {
19251 return Err(self.err(alloc::format!(
19252 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19253 )));
19254 }
19255 }
19256 let mut assignments: Vec<(String, Expr)> = Vec::new();
19257 loop {
19258 let col = self.expect_ident_like()?;
19259 if !matches!(self.peek(), Token::Eq) {
19260 return Err(self.err(alloc::format!(
19261 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19262 self.peek()
19263 )));
19264 }
19265 self.advance();
19266 let value = self.parse_expr(0)?;
19267 assignments.push((col, value));
19268 if matches!(self.peek(), Token::Comma) {
19269 self.advance();
19270 continue;
19271 }
19272 break;
19273 }
19274 let where_ = if matches!(self.peek(), Token::Where) {
19275 self.advance();
19276 Some(self.parse_expr(0)?)
19277 } else {
19278 None
19279 };
19280 Ok(crate::ast::OnConflictAction::Update {
19281 assignments,
19282 where_,
19283 })
19284 }
19285
19286 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19287 let mut items = Vec::new();
19288 // v7.39 (round 341, V66) — PG's target list may be EMPTY
19289 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19290 // answers one zero-column row per row of t, and a bare `SELECT`
19291 // answers a single zero-column row. SPG required at least one
19292 // item, so both were syntax errors. Recognised by the token that
19293 // follows — nothing that can start an expression appears here.
19294 if self.select_list_is_empty_here() {
19295 return Ok(items);
19296 }
19297 loop {
19298 items.push(self.parse_select_item()?);
19299 if matches!(self.peek(), Token::Comma) {
19300 self.advance();
19301 } else {
19302 break;
19303 }
19304 }
19305 Ok(items)
19306 }
19307
19308 /// Is the target list empty at this point — i.e. does the next token
19309 /// end the SELECT's item list rather than start an item?
19310 fn select_list_is_empty_here(&self) -> bool {
19311 match self.peek() {
19312 Token::From
19313 | Token::Where
19314 | Token::Group
19315 | Token::Having
19316 | Token::Order
19317 | Token::Limit
19318 | Token::Offset
19319 | Token::Semicolon
19320 | Token::RParen
19321 | Token::Union
19322 | Token::Except
19323 | Token::Eof => true,
19324 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19325 // with unreserved keywords, so they arrive as plain idents.
19326 Token::Ident(s) => {
19327 s.eq_ignore_ascii_case("fetch")
19328 || s.eq_ignore_ascii_case("window")
19329 || s.eq_ignore_ascii_case("intersect")
19330 }
19331 _ => false,
19332 }
19333 }
19334
19335 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19336 if matches!(self.peek(), Token::Star) {
19337 self.advance();
19338 return Ok(SelectItem::Wildcard);
19339 }
19340 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19341 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19342 // choke on the `*` ("expected identifier, got Star"). The lookahead is
19343 // `<ident> . *` with nothing binding tighter.
19344 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19345 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19346 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19347 {
19348 self.advance(); // qualifier
19349 self.advance(); // .
19350 self.advance(); // *
19351 return Ok(SelectItem::QualifiedWildcard(q));
19352 }
19353 }
19354 let start_tok = self.pos;
19355 let expr = self.parse_expr(0)?;
19356 let end_tok = self.consumed_pos();
19357 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19358 // multi-column function returns into columns. Marked here and lowered in
19359 // `parse_bare_select`, where the FROM clause is in hand.
19360 if matches!(self.peek(), Token::Dot)
19361 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19362 {
19363 self.advance(); // .
19364 self.advance(); // *
19365 return Ok(SelectItem::Expr {
19366 expr: Expr::FunctionCall {
19367 name: "__record_expand".to_string(),
19368 args: alloc::vec![expr],
19369 },
19370 alias: None,
19371 });
19372 }
19373 // v7.39.2 — MySQL lets a STRING name a projection item, with or
19374 // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19375 // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19376 // `syntax error at or near "'x'"` to all of them.
19377 //
19378 // Only here, not in `parse_optional_alias`: that one also names
19379 // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19380 // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19381 // after the lexer's own rule has joined adjacent literals, or
19382 // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19383 // MySQL answers the concatenation `ab`.
19384 if self.mysql_dialect {
19385 let at_as = matches!(self.peek(), Token::As)
19386 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19387 if at_as {
19388 self.advance();
19389 }
19390 if let Token::String(name) = self.peek().clone() {
19391 self.advance();
19392 return Ok(SelectItem::Expr {
19393 expr,
19394 alias: Some(name),
19395 });
19396 }
19397 }
19398 let alias = match self.parse_optional_alias()? {
19399 Some(a) => Some(a),
19400 None => self.mysql_item_label(&expr, start_tok, end_tok),
19401 };
19402 Ok(SelectItem::Expr { expr, alias })
19403 }
19404
19405 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19406 /// carries no `AS`, filled in here so every downstream path reports it
19407 /// without knowing the rule. `None` leaves the item un-aliased, which is
19408 /// what a PG session always gets.
19409 ///
19410 /// Measured against MariaDB 11, three rules and no more:
19411 ///
19412 /// | item | label | why |
19413 /// |------------------|------------|------------------------------|
19414 /// | `lbl.a` | `a` | a column reports its name |
19415 /// | `'it''s'` | `it's` | a string reports its VALUE |
19416 /// | `a + b` | `a + b` | anything else, source text |
19417 ///
19418 /// The third is why this lives in the parser at all: the label is the
19419 /// text the client WROTE, down to the spacing, so it cannot be printed
19420 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19421 ///
19422 /// Comments survive, and that is right: through a `mariadb` CLI both
19423 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
19424 /// CLIENT stripping the comment before it sends. Asked over the raw
19425 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19426 /// produces.
19427 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19428 if !self.mysql_dialect {
19429 return None;
19430 }
19431 match expr {
19432 // A column already reports its own name downstream; naming it
19433 // again here would only re-state the qualifier the label drops.
19434 Expr::Column(_) => None,
19435 // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19436 // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19437 // the first segment as written, not the joined value
19438 // (measured). The lexer logs where it joined them.
19439 Expr::Literal(Literal::String(v)) => Some(
19440 self.merged_first_len(start_tok)
19441 .and_then(|n| v.get(..n))
19442 .map_or_else(|| v.clone(), String::from),
19443 ),
19444 _ => self.source_span(start_tok, end_tok).map(str::to_string),
19445 }
19446 }
19447
19448 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19449 /// consumed VALUES keyword. Each row lowers to a constant SELECT
19450 /// with PG's default column1..columnN names; subsequent rows
19451 /// chain as UNION ALL peers. Shared by the FROM-position
19452 /// `( VALUES … )` arm and the top-level bare VALUES statement.
19453 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19454 let mut row_selects: Vec<SelectStatement> = Vec::new();
19455 loop {
19456 if !matches!(self.peek(), Token::LParen) {
19457 return Err(self.err(alloc::format!(
19458 "expected '(' to start a VALUES row, got {:?}",
19459 self.peek()
19460 )));
19461 }
19462 self.advance(); // (
19463 let mut items: Vec<SelectItem> = Vec::new();
19464 loop {
19465 let expr = self.parse_expr(0)?;
19466 items.push(SelectItem::Expr {
19467 expr,
19468 alias: Some(alloc::format!("column{}", items.len() + 1)),
19469 });
19470 match self.peek() {
19471 Token::Comma => {
19472 self.advance();
19473 }
19474 Token::RParen => break,
19475 other => {
19476 return Err(self.err(alloc::format!(
19477 "expected ',' or ')' in VALUES row, got {other:?}"
19478 )));
19479 }
19480 }
19481 }
19482 self.advance(); // )
19483 row_selects.push(SelectStatement {
19484 locking: None,
19485 ctes: Vec::new(),
19486 distinct: false,
19487 distinct_on: Vec::new(),
19488 items,
19489 from: None,
19490 where_: None,
19491 group_by: None,
19492 group_by_all: false,
19493 having: None,
19494 unions: Vec::new(),
19495 order_by: Vec::new(),
19496 limit: None,
19497 offset: None,
19498 limit_with_ties: false,
19499 window_check_exprs: Vec::new(),
19500 });
19501 if matches!(self.peek(), Token::Comma) {
19502 self.advance();
19503 continue;
19504 }
19505 break;
19506 }
19507 let mut head = row_selects.remove(0);
19508 head.unions = row_selects
19509 .into_iter()
19510 .map(|s| (UnionKind::All, s))
19511 .collect();
19512 Ok(head)
19513 }
19514
19515 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19516 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19517 // children. It was read as a table NAMED `only`, so the query
19518 // failed on `relation "only" does not exist`.
19519 //
19520 // v7.39 (round 644) — and it is no longer a no-op. Round 621
19521 // absorbed the keyword, reasoning that SPG's children are
19522 // separate relations a plain scan does not descend into, so ONLY
19523 // already described the scan. That stopped being true when a
19524 // partition parent started unioning its children: measured,
19525 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19526 // where PG answers 0. The flag is carried now.
19527 let mut only = false;
19528 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19529 && matches!(
19530 self.tokens.get(self.pos + 1),
19531 Some(Token::Ident(_) | Token::QuotedIdent(_))
19532 )
19533 {
19534 only = true;
19535 self.advance();
19536 }
19537 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19538 // for these SRFs the keyword is noise at parse time: the
19539 // join executor already substitutes outer-column references
19540 // into unnest_expr / generate_series_args per outer row
19541 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19542 // licences the correlation even without the keyword. Absorb
19543 // it and fall through to the SRF arms below.
19544 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19545 // just the four builtin SRFs: a user set-returning function on a JOIN's
19546 // right side is the whole point of LATERAL. The keyword stays noise at
19547 // parse time — the join executor substitutes the outer row into the
19548 // call's arguments per outer row.
19549 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19550 && matches!(
19551 self.tokens.get(self.pos + 1),
19552 // The json_each family has its OWN `LATERAL …` arm below, which
19553 // needs to see the keyword — absorbing it here would send those
19554 // calls down the generic table-function channel instead.
19555 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19556 )
19557 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19558 {
19559 self.advance(); // LATERAL
19560 }
19561 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19562 // set-returning function whose argument may reference a
19563 // preceding FROM item. We rewrite this to
19564 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19565 // AS __srf__) AS <alias>` so the existing LATERAL subquery
19566 // executor handles per-outer-row evaluation and the
19567 // SRF-primary jsonb_each_text path handles the inner
19568 // materialisation. Sentori 0067 backfill is the dogfood
19569 // shape.
19570 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19571 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19572 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19573 {
19574 self.advance(); // LATERAL
19575 let each_fn = match self.peek() {
19576 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19577 _ => unreachable!(),
19578 };
19579 self.advance(); // jsonb_each[_text] / json_each[_text]
19580 self.advance(); // (
19581 let arg = self.parse_expr(0)?;
19582 if !matches!(self.peek(), Token::RParen) {
19583 return Err(self.err(alloc::format!(
19584 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19585 self.peek()
19586 )));
19587 }
19588 self.advance();
19589 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19590 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19591 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19592 // FROM jsonb_each_text(<arg>) AS __srf__
19593 // PG's `AS kv(key, value)` column-alias list maps
19594 // positions to names; default to (key, value) when
19595 // omitted (matching the SRF's natural column names).
19596 let srf_alias = "__srf__".to_string();
19597 let key_alias = column_aliases
19598 .first()
19599 .cloned()
19600 .unwrap_or_else(|| "key".to_string());
19601 let value_alias = column_aliases
19602 .get(1)
19603 .cloned()
19604 .unwrap_or_else(|| "value".to_string());
19605 let inner_select = crate::ast::SelectStatement {
19606 locking: None,
19607 ctes: Vec::new(),
19608 distinct: false,
19609 distinct_on: Vec::new(),
19610 items: alloc::vec![
19611 crate::ast::SelectItem::Expr {
19612 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19613 qualifier: Some(srf_alias.clone()),
19614 name: "key".to_string(),
19615 }),
19616 alias: Some(key_alias),
19617 },
19618 crate::ast::SelectItem::Expr {
19619 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19620 qualifier: Some(srf_alias.clone()),
19621 name: "value".to_string(),
19622 }),
19623 alias: Some(value_alias),
19624 },
19625 ],
19626 from: Some(crate::ast::FromClause {
19627 primary: TableRef {
19628 name: srf_alias.clone(),
19629 alias: Some(srf_alias.clone()),
19630 only: false,
19631 as_of_segment: None,
19632 unnest_expr: None,
19633 unnest_column_aliases: Vec::new(),
19634 with_ordinality: false,
19635 generate_series_args: None,
19636 lateral_subquery: None,
19637 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19638 table_fn_call: None,
19639 rows_from: None,
19640 json_table: None,
19641 scalar_fn_item: false,
19642 },
19643 joins: Vec::new(),
19644 }),
19645 where_: None,
19646 group_by: None,
19647 group_by_all: false,
19648 having: None,
19649 unions: Vec::new(),
19650 order_by: Vec::new(),
19651 limit: None,
19652 offset: None,
19653 limit_with_ties: false,
19654 window_check_exprs: Vec::new(),
19655 };
19656 return Ok(TableRef {
19657 name: alias.clone(),
19658 alias: Some(alias),
19659 only: false,
19660 as_of_segment: None,
19661 unnest_expr: None,
19662 unnest_column_aliases: Vec::new(),
19663 with_ordinality: false,
19664 generate_series_args: None,
19665 lateral_subquery: Some(Box::new(inner_select)),
19666 jsonb_each_text_arg: None,
19667 table_fn_call: None,
19668 rows_from: None,
19669 json_table: None,
19670 scalar_fn_item: false,
19671 });
19672 }
19673 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19674 // without an explicit `LATERAL` keyword is the same shape
19675 // PG accepts (SRF naturally licences lateral correlation).
19676 // We mirror the LATERAL rewrite when the argument syntactic-
19677 // ally references an outer column (Column { qualifier:
19678 // Some(_), … }). For simplicity we apply the rewrite
19679 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19680 // in the FROM-list — caller-side join parsing positions
19681 // this peek correctly.
19682 // (Implementation note: detection lives below; the LATERAL
19683 // branch above already covers the explicit form; the bare
19684 // form falls through to the plain SRF arm and the engine
19685 // treats it as a constant-arg SRF if no outer reference is
19686 // present.)
19687 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19688 // table. Detect at the head so it claims precedence over
19689 // every other table-ref shape (unnest / generate_series /
19690 // bare ident); the lateral subquery itself follows the
19691 // regular SELECT grammar.
19692 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19693 // t(cols)`. Each row lowers to a constant SELECT with PG's
19694 // default column1..columnN names; subsequent rows chain as
19695 // UNION ALL peers. The result rides the derived-table
19696 // lateral_subquery channel — zero executor work.
19697 if matches!(self.peek(), Token::LParen)
19698 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19699 {
19700 self.advance(); // (
19701 self.advance(); // VALUES
19702 let head = self.parse_values_rows_body()?;
19703 if !matches!(self.peek(), Token::RParen) {
19704 return Err(self.err(alloc::format!(
19705 "expected ')' after VALUES list, got {:?}",
19706 self.peek()
19707 )));
19708 }
19709 self.advance();
19710 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19711 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19712 return Ok(TableRef {
19713 name,
19714 alias: alias_ident,
19715 only: false,
19716 as_of_segment: None,
19717 unnest_expr: None,
19718 unnest_column_aliases: column_aliases,
19719 with_ordinality: false,
19720 generate_series_args: None,
19721 lateral_subquery: Some(Box::new(head)),
19722 jsonb_each_text_arg: None,
19723 table_fn_call: None,
19724 rows_from: None,
19725 json_table: None,
19726 scalar_fn_item: false,
19727 });
19728 }
19729 // v7.37.17 (17.6 siblings) — plain derived table:
19730 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19731 // lateral_subquery channel the explicit LATERAL form uses —
19732 // an uncorrelated inner SELECT executes identically. The
19733 // inner parse carries UNION tails (they live on
19734 // SelectStatement.unions).
19735 // v7.37 D.20 — the derived-table inner may itself be a
19736 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19737 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19738 // bare `(SELECT …)`. parse_one_statement already routes a leading
19739 // `(` set-op group (its LParen arm) and a leading WITH
19740 // (parse_with_cte_then_select), so widen the second-token gate to
19741 // Select | LParen | WITH.
19742 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19743 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19744 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19745 // has existed since the shorthand landed and `parse_bare_select`
19746 // already routes it ("valid anywhere a SELECT head is"); what was
19747 // missing is this second-token gate, and the CTE body's dispatch
19748 // below. Round 868 found both by putting the shorthand in a
19749 // subquery — the top-level forms had been the only ones tested.
19750 if matches!(self.peek(), Token::LParen)
19751 && (matches!(
19752 self.tokens.get(self.pos + 1),
19753 Some(Token::Select | Token::LParen | Token::Table)
19754 ) || matches!(self.tokens.get(self.pos + 1),
19755 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19756 {
19757 self.advance(); // (
19758 let inner = match self.parse_one_statement()? {
19759 Statement::Select(s) => s,
19760 other => {
19761 return Err(self.err(alloc::format!(
19762 "expected SELECT inside derived table ( … ), got {other:?}"
19763 )));
19764 }
19765 };
19766 if !matches!(self.peek(), Token::RParen) {
19767 return Err(self.err(alloc::format!(
19768 "expected ')' after derived-table subquery, got {:?}",
19769 self.peek()
19770 )));
19771 }
19772 self.advance();
19773 // `AS t(a, b)` column-alias list rides the
19774 // unnest_column_aliases field (same positional-rename
19775 // contract the unnest SRFs use).
19776 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19777 let name = alias_ident
19778 .clone()
19779 .unwrap_or_else(|| "subquery".to_string());
19780 return Ok(TableRef {
19781 name,
19782 alias: alias_ident,
19783 only: false,
19784 as_of_segment: None,
19785 unnest_expr: None,
19786 unnest_column_aliases: column_aliases,
19787 with_ordinality: false,
19788 generate_series_args: None,
19789 lateral_subquery: Some(Box::new(inner)),
19790 jsonb_each_text_arg: None,
19791 table_fn_call: None,
19792 rows_from: None,
19793 json_table: None,
19794 scalar_fn_item: false,
19795 });
19796 }
19797 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19798 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19799 {
19800 self.advance(); // LATERAL
19801 self.advance(); // (
19802 // Parse the inner SELECT.
19803 let inner = match self.parse_one_statement()? {
19804 Statement::Select(s) => s,
19805 other => {
19806 return Err(self.err(alloc::format!(
19807 "expected SELECT inside LATERAL ( … ), got {other:?}"
19808 )));
19809 }
19810 };
19811 if !matches!(self.peek(), Token::RParen) {
19812 return Err(self.err(alloc::format!(
19813 "expected ')' after LATERAL subquery, got {:?}",
19814 self.peek()
19815 )));
19816 }
19817 self.advance();
19818 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19819 // `(VALUES …) t(g)` derived table round-trips through view-body
19820 // Display, which renders on the lateral_subquery channel).
19821 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19822 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19823 return Ok(TableRef {
19824 name,
19825 alias: alias_ident,
19826 only: false,
19827 as_of_segment: None,
19828 unnest_expr: None,
19829 unnest_column_aliases: column_aliases,
19830 with_ordinality: false,
19831 generate_series_args: None,
19832 lateral_subquery: Some(Box::new(inner)),
19833 jsonb_each_text_arg: None,
19834 table_fn_call: None,
19835 rows_from: None,
19836 json_table: None,
19837 scalar_fn_item: false,
19838 });
19839 }
19840 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19841 // function as a FROM item. Emits one row per (key, value)
19842 // pair in the JSONB object argument as TEXT columns. May
19843 // be wrapped in CROSS JOIN LATERAL when the argument
19844 // references a preceding FROM item (sentori migration
19845 // 0067 backfill shape: `CROSS JOIN LATERAL
19846 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19847 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19848 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19849 {
19850 let each_fn = match self.peek() {
19851 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19852 _ => unreachable!(),
19853 };
19854 self.advance(); // jsonb_each[_text] / json_each[_text]
19855 self.advance(); // (
19856 let arg = self.parse_expr(0)?;
19857 if !matches!(self.peek(), Token::RParen) {
19858 return Err(self.err(alloc::format!(
19859 "expected ')' after {each_fn}() argument, got {:?}",
19860 self.peek()
19861 )));
19862 }
19863 self.advance();
19864 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19865 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19866 return Ok(TableRef {
19867 name,
19868 alias: alias_ident,
19869 only: false,
19870 as_of_segment: None,
19871 unnest_expr: None,
19872 // `AS t(k, v)` renames key/value positionally, same as the
19873 // LATERAL-position form already does.
19874 unnest_column_aliases: column_aliases,
19875 with_ordinality: false,
19876 generate_series_args: None,
19877 lateral_subquery: None,
19878 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19879 table_fn_call: None,
19880 rows_from: None,
19881 json_table: None,
19882 scalar_fn_item: false,
19883 });
19884 }
19885 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19886 // (+ json_ variants) — record-returning JSON functions with a
19887 // column-definition list. Desugar to a derived table that
19888 // projects each declared column from the JSON via `->>` + a cast,
19889 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19890 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19891 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19892 {
19893 return self.parse_json_to_record_from();
19894 }
19895 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19896 // row is a text[] of capture groups, so it cannot desugar to unnest
19897 // (that would flatten the array). Wrap it as a derived table
19898 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19899 // SRF path already emits one text[] row per match. PG names the column
19900 // `regexp_matches`; an `AS a(col)` alias overrides it.
19901 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19902 if s.eq_ignore_ascii_case("regexp_matches"))
19903 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19904 {
19905 self.advance(); // fn name
19906 self.advance(); // (
19907 let mut fn_args: Vec<Expr> = Vec::new();
19908 loop {
19909 fn_args.push(self.parse_expr(0)?);
19910 if matches!(self.peek(), Token::Comma) {
19911 self.advance();
19912 continue;
19913 }
19914 break;
19915 }
19916 if !matches!(self.peek(), Token::RParen) {
19917 return Err(self.err(alloc::format!(
19918 "expected ')' after regexp_matches() arguments, got {:?}",
19919 self.peek()
19920 )));
19921 }
19922 self.advance();
19923 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19924 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19925 // it, so it died on the `with` token while every other table function
19926 // accepted it.
19927 let with_ordinality = self.absorb_with_ordinality();
19928 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19929 let table_alias = alias_ident
19930 .clone()
19931 .unwrap_or_else(|| "regexp_matches".to_string());
19932 // PG names a single-column function's output column after the ALIAS
19933 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19934 // `m` reads as that column and not as a whole-row composite. Naming
19935 // it after the function regardless made `SELECT m[1] FROM … AS m`
19936 // subscript a record.
19937 let col_name = column_aliases
19938 .first()
19939 .cloned()
19940 .or_else(|| alias_ident.clone())
19941 .unwrap_or_else(|| "regexp_matches".to_string());
19942 let inner = crate::ast::SelectStatement {
19943 locking: None,
19944 ctes: Vec::new(),
19945 distinct: false,
19946 distinct_on: Vec::new(),
19947 items: alloc::vec![SelectItem::Expr {
19948 expr: Expr::FunctionCall {
19949 name: "regexp_matches".to_string(),
19950 args: fn_args,
19951 },
19952 alias: Some(col_name),
19953 }],
19954 from: None,
19955 where_: None,
19956 group_by: None,
19957 group_by_all: false,
19958 having: None,
19959 unions: Vec::new(),
19960 order_by: Vec::new(),
19961 limit: None,
19962 offset: None,
19963 limit_with_ties: false,
19964 window_check_exprs: Vec::new(),
19965 };
19966 return Ok(TableRef {
19967 name: table_alias.clone(),
19968 alias: Some(table_alias),
19969 only: false,
19970 as_of_segment: None,
19971 unnest_expr: None,
19972 unnest_column_aliases: column_aliases,
19973 with_ordinality,
19974 generate_series_args: None,
19975 lateral_subquery: Some(Box::new(inner)),
19976 jsonb_each_text_arg: None,
19977 table_fn_call: None,
19978 rows_from: None,
19979 json_table: None,
19980 // regexp_matches returns text[], a base type: `SELECT m FROM
19981 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19982 scalar_fn_item: true,
19983 });
19984 }
19985 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19986 // / json_ variants as a FROM item. Rewritten into
19987 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19988 // elements as a TEXT array, and the existing unnest SRF path
19989 // materialises one row per element. PG's natural column name
19990 // is `value`; an `AS a(col)` column-alias list overrides it.
19991 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19992 if s.eq_ignore_ascii_case("jsonb_array_elements")
19993 || s.eq_ignore_ascii_case("json_array_elements")
19994 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19995 || s.eq_ignore_ascii_case("json_array_elements_text")
19996 || s.eq_ignore_ascii_case("jsonb_object_keys")
19997 || s.eq_ignore_ascii_case("json_object_keys")
19998 || s.eq_ignore_ascii_case("jsonb_path_query")
19999 || s.eq_ignore_ascii_case("json_path_query")
20000 || s.eq_ignore_ascii_case("generate_subscripts")
20001 || s.eq_ignore_ascii_case("string_to_table")
20002 || s.eq_ignore_ascii_case("regexp_split_to_table"))
20003 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20004 {
20005 let fn_name = match self.peek() {
20006 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20007 _ => unreachable!(),
20008 };
20009 self.advance(); // fn name
20010 self.advance(); // (
20011 let mut fn_args: Vec<Expr> = Vec::new();
20012 loop {
20013 fn_args.push(self.parse_expr(0)?);
20014 if matches!(self.peek(), Token::Comma) {
20015 self.advance();
20016 continue;
20017 }
20018 break;
20019 }
20020 if !matches!(self.peek(), Token::RParen) {
20021 return Err(self.err(alloc::format!(
20022 "expected ')' after {fn_name}() arguments, got {:?}",
20023 self.peek()
20024 )));
20025 }
20026 self.advance();
20027 let with_ordinality = self.absorb_with_ordinality();
20028 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20029 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20030 // PG's natural column name: the array-elements SRFs
20031 // declare an OUT parameter `value`; jsonb_object_keys
20032 // and generate_subscripts have none, so the column is
20033 // named after the function. A bare table alias on a
20034 // single-column SRF renames the column too (PG: `FROM
20035 // generate_subscripts(a, 1) AS s` projects column s) —
20036 // except for the OUT-parameter SRFs, whose column stays
20037 // `value` under a bare alias.
20038 let natural_col = if fn_name.ends_with("_array_elements")
20039 || fn_name.ends_with("_array_elements_text")
20040 {
20041 "value".to_string()
20042 } else {
20043 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
20044 };
20045 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
20046 // Keep any further entries — the second names the
20047 // ordinality column under WITH ORDINALITY.
20048 srf_cols.extend(column_aliases.into_iter().skip(1));
20049 // The *_to_table SRFs are row-streams over the existing
20050 // *_to_array scalars — map the call target; the display
20051 // name (alias / column defaults) keeps the SRF spelling.
20052 let call_name = match fn_name.as_str() {
20053 "string_to_table" => "string_to_array".to_string(),
20054 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
20055 _ => fn_name,
20056 };
20057 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
20058 // preceding FROM item (bare or qualified column) is correlated;
20059 // route it through the per-outer-row lateral channel.
20060 let expr = crate::ast::Expr::FunctionCall {
20061 name: call_name,
20062 args: fn_args,
20063 };
20064 let correlated = Self::expr_has_any_column(&expr);
20065 let tref = TableRef {
20066 name,
20067 alias: alias_ident,
20068 only: false,
20069 as_of_segment: None,
20070 unnest_expr: Some(Box::new(expr)),
20071 unnest_column_aliases: srf_cols,
20072 with_ordinality,
20073 generate_series_args: None,
20074 lateral_subquery: None,
20075 jsonb_each_text_arg: None,
20076 table_fn_call: None,
20077 rows_from: None,
20078 json_table: None,
20079 // Each of these returns a BASE type (jsonb / text / int), so the item's
20080 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
20081 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
20082 scalar_fn_item: !with_ordinality,
20083 };
20084 return Ok(if correlated {
20085 Self::wrap_correlated_srf(tref)
20086 } else {
20087 tref
20088 });
20089 }
20090 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
20091 // explicit parallel-zip syntax. Each entry lowers to its
20092 // array-returning scalar form (unnest(x) → x itself; the
20093 // FROM-SRF rewrite family → their scalar array calls) and
20094 // the list rides the multi-arg unnest zip channel:
20095 // NULL-padded to the longest, WITH ORDINALITY appends the
20096 // counter. generate_series has no scalar array form and
20097 // errors honestly.
20098 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20099 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20100 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20101 {
20102 self.advance(); // ROWS
20103 self.advance(); // FROM
20104 self.advance(); // (
20105 let mut entries: Vec<Expr> = Vec::new();
20106 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20107 // Used only when some entry has no array form.
20108 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20109 loop {
20110 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20111 if !matches!(self.peek(), Token::LParen) {
20112 return Err(self.err(alloc::format!(
20113 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20114 self.peek()
20115 )));
20116 }
20117 self.advance();
20118 let mut fn_args: Vec<Expr> = Vec::new();
20119 if !matches!(self.peek(), Token::RParen) {
20120 loop {
20121 fn_args.push(self.parse_expr(0)?);
20122 if matches!(self.peek(), Token::Comma) {
20123 self.advance();
20124 continue;
20125 }
20126 break;
20127 }
20128 }
20129 if !matches!(self.peek(), Token::RParen) {
20130 return Err(self.err(alloc::format!(
20131 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20132 self.peek()
20133 )));
20134 }
20135 self.advance();
20136 let entry = match fn_name.as_str() {
20137 "unnest" => {
20138 if fn_args.len() != 1 {
20139 return Err(
20140 self.err("unnest inside ROWS FROM takes exactly one array".into())
20141 );
20142 }
20143 fn_args.pop().expect("len checked")
20144 }
20145 "jsonb_array_elements"
20146 | "json_array_elements"
20147 | "jsonb_array_elements_text"
20148 | "json_array_elements_text"
20149 | "jsonb_object_keys"
20150 | "json_object_keys"
20151 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20152 name: fn_name,
20153 args: fn_args,
20154 },
20155 "string_to_table" => crate::ast::Expr::FunctionCall {
20156 name: "string_to_array".to_string(),
20157 args: fn_args,
20158 },
20159 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20160 name: "regexp_split_to_array".to_string(),
20161 args: fn_args,
20162 },
20163 // v7.39 (read01 round 74) — an SRF with no array form
20164 // (`generate_series`, a user `RETURNS SETOF` function) has no
20165 // scalar expression to zip, so the WHOLE list switches to the
20166 // rows_from channel, which runs each function and zips the
20167 // rows themselves. The all-array case keeps the old lowering:
20168 // it is well-trodden and this must not disturb it.
20169 _ => {
20170 generic.push((fn_name, fn_args));
20171 if matches!(self.peek(), Token::Comma) {
20172 self.advance();
20173 continue;
20174 }
20175 break;
20176 }
20177 };
20178 generic.push((
20179 // The array-able entries carry their lowered expr along, so a
20180 // MIXED list still works: the engine sees the scalar array
20181 // form and unnests it.
20182 "__array".to_string(),
20183 alloc::vec![entry.clone()],
20184 ));
20185 entries.push(entry);
20186 if matches!(self.peek(), Token::Comma) {
20187 self.advance();
20188 continue;
20189 }
20190 break;
20191 }
20192 if !matches!(self.peek(), Token::RParen) {
20193 return Err(self.err(alloc::format!(
20194 "expected ')' to close ROWS FROM, got {:?}",
20195 self.peek()
20196 )));
20197 }
20198 self.advance();
20199 let with_ordinality = self.absorb_with_ordinality();
20200 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20201 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20202 // v7.39 (read01 round 74) — some entry had no array form, so the whole
20203 // list rides the generic channel.
20204 if generic.iter().any(|(n, _)| n != "__array") {
20205 let correlated = generic
20206 .iter()
20207 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20208 let tref = TableRef {
20209 name,
20210 alias: alias_ident,
20211 only: false,
20212 as_of_segment: None,
20213 unnest_expr: None,
20214 unnest_column_aliases,
20215 with_ordinality,
20216 generate_series_args: None,
20217 lateral_subquery: None,
20218 jsonb_each_text_arg: None,
20219 table_fn_call: None,
20220 rows_from: Some(generic),
20221 json_table: None,
20222 scalar_fn_item: false,
20223 };
20224 return Ok(if correlated {
20225 Self::wrap_correlated_srf(tref)
20226 } else {
20227 tref
20228 });
20229 }
20230 let correlated = entries.iter().any(Self::expr_has_any_column);
20231 let expr = if entries.len() == 1 {
20232 entries.pop().expect("len checked")
20233 } else {
20234 crate::ast::Expr::FunctionCall {
20235 name: "__unnest_zip".to_string(),
20236 args: entries,
20237 }
20238 };
20239 let tref = TableRef {
20240 name,
20241 alias: alias_ident,
20242 only: false,
20243 as_of_segment: None,
20244 unnest_expr: Some(Box::new(expr)),
20245 unnest_column_aliases,
20246 with_ordinality,
20247 generate_series_args: None,
20248 lateral_subquery: None,
20249 jsonb_each_text_arg: None,
20250 table_fn_call: None,
20251 rows_from: None,
20252 json_table: None,
20253 scalar_fn_item: false,
20254 };
20255 return Ok(if correlated {
20256 Self::wrap_correlated_srf(tref)
20257 } else {
20258 tref
20259 });
20260 }
20261 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20262 // source. Detect at the head before the bare-ident fallback;
20263 // unnest is not a reserved token.
20264 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20265 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20266 {
20267 self.advance(); // unnest
20268 self.advance(); // (
20269 let mut srf_args = alloc::vec![self.parse_expr(0)?];
20270 while matches!(self.peek(), Token::Comma) {
20271 self.advance();
20272 srf_args.push(self.parse_expr(0)?);
20273 }
20274 if !matches!(self.peek(), Token::RParen) {
20275 return Err(self.err(alloc::format!(
20276 "expected ')' after unnest() argument, got {:?}",
20277 self.peek()
20278 )));
20279 }
20280 self.advance();
20281 // Multi-arg unnest(a, b, …) zips the arrays in
20282 // parallel, NULL-padding to the longest (PG's ROWS
20283 // FROM shorthand). Lower onto the unnest channel as an
20284 // internal marker call the executors unpack.
20285 let expr = if srf_args.len() == 1 {
20286 srf_args.pop().expect("len checked")
20287 } else {
20288 crate::ast::Expr::FunctionCall {
20289 name: "__unnest_zip".to_string(),
20290 args: srf_args,
20291 }
20292 };
20293 let with_ordinality = self.absorb_with_ordinality();
20294 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20295 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20296 let correlated = Self::expr_has_any_column(&expr);
20297 let tref = TableRef {
20298 name,
20299 alias: alias_ident,
20300 only: false,
20301 as_of_segment: None,
20302 unnest_expr: Some(Box::new(expr)),
20303 unnest_column_aliases,
20304 with_ordinality,
20305 generate_series_args: None,
20306 lateral_subquery: None,
20307 jsonb_each_text_arg: None,
20308 table_fn_call: None,
20309 rows_from: None,
20310 json_table: None,
20311 scalar_fn_item: false,
20312 };
20313 return Ok(if correlated {
20314 Self::wrap_correlated_srf(tref)
20315 } else {
20316 tref
20317 });
20318 }
20319 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20320 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20321 // generic table-fn arg parser can't read), so it is intercepted
20322 // here BEFORE the generic dispatch. The doc expr may reference
20323 // outer columns (implicit LATERAL) — same correlated-wrap rule.
20324 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20325 if s.eq_ignore_ascii_case("json_table"))
20326 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20327 {
20328 let tref = self.parse_json_table_ref()?;
20329 let correlated = tref
20330 .json_table
20331 .as_deref()
20332 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20333 return Ok(if correlated {
20334 Self::wrap_correlated_srf(tref)
20335 } else {
20336 tref
20337 });
20338 }
20339 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20340 // functions dispatched by name (`pg_partition_tree('t')`,
20341 // `pg_partition_ancestors('t')`). Same head-detection shape as
20342 // unnest; the engine executor owns the row shape per function.
20343 // v7.39 (read01 round 65) — and a USER function in FROM position
20344 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20345 // (generate_series / unnest / the json_each family) keep it — their arms
20346 // sit further down, so they are excluded here by name rather than by
20347 // ordering. Anything else that is an ident followed by `(` is a table
20348 // function; the engine executor decides whether it is a builtin, a
20349 // set-returning user function, or an error.
20350 // 7.38.1 S5.1 — pg_dump spells its table functions
20351 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20352 // strip the pg_catalog prefix here so the same head-detection
20353 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20354 // meaning.
20355 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20356 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20357 && matches!(
20358 self.tokens.get(self.pos + 2),
20359 Some(Token::Ident(_) | Token::QuotedIdent(_))
20360 )
20361 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20362 {
20363 self.advance(); // pg_catalog
20364 self.advance(); // .
20365 }
20366 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20367 if !s.eq_ignore_ascii_case("generate_series")
20368 && !s.eq_ignore_ascii_case("unnest")
20369 && !is_json_each_name(s))
20370 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20371 {
20372 // Body out-of-line — this parse sits on the FROM/subquery
20373 // recursion chain (debug frame-cliff discipline).
20374 // v7.39 (read01 round 69) — a call whose arguments reference an outer
20375 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20376 // outer row, so it rides the lateral channel. Same rule the unnest
20377 // arm uses.
20378 let tref = self.parse_table_fn_ref()?;
20379 let correlated = tref
20380 .table_fn_call
20381 .as_deref()
20382 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20383 return Ok(if correlated {
20384 Self::wrap_correlated_srf(tref)
20385 } else {
20386 tref
20387 });
20388 }
20389 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20390 // [, step])` set-returning source. Same shape as unnest:
20391 // detect at the head, parse the comma-separated arg list,
20392 // dispatch downstream through the engine's set-returning
20393 // path. Supports integer triplets (mailrs's `WITH row_no AS
20394 // (SELECT * FROM generate_series(1, N))` pattern) and
20395 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20396 // date-range iteration pattern, which pre-3.10 had no
20397 // direct equivalent in SPG).
20398 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20399 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20400 {
20401 self.advance(); // generate_series
20402 self.advance(); // (
20403 let mut args: Vec<Expr> = Vec::new();
20404 loop {
20405 args.push(self.parse_expr(0)?);
20406 if matches!(self.peek(), Token::Comma) {
20407 self.advance();
20408 continue;
20409 }
20410 break;
20411 }
20412 if !matches!(self.peek(), Token::RParen) {
20413 return Err(self.err(alloc::format!(
20414 "expected ')' after generate_series() arguments, got {:?}",
20415 self.peek()
20416 )));
20417 }
20418 self.advance();
20419 if args.len() < 2 || args.len() > 3 {
20420 return Err(self.err(alloc::format!(
20421 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20422 args.len()
20423 )));
20424 }
20425 let with_ordinality = self.absorb_with_ordinality();
20426 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20427 let name = alias_ident
20428 .clone()
20429 .unwrap_or_else(|| "generate_series".to_string());
20430 let correlated = args.iter().any(Self::expr_has_any_column);
20431 let tref = TableRef {
20432 name,
20433 alias: alias_ident,
20434 only: false,
20435 as_of_segment: None,
20436 unnest_expr: None,
20437 unnest_column_aliases: column_aliases,
20438 with_ordinality,
20439 generate_series_args: Some(args),
20440 lateral_subquery: None,
20441 jsonb_each_text_arg: None,
20442 table_fn_call: None,
20443 rows_from: None,
20444 json_table: None,
20445 scalar_fn_item: false,
20446 };
20447 return Ok(if correlated {
20448 Self::wrap_correlated_srf(tref)
20449 } else {
20450 tref
20451 });
20452 }
20453 // v7.16.2 — preserve information_schema / pg_catalog
20454 // qualifiers (mailrs round-10 A.3). The generic
20455 // `expect_ident_like` strip silently drops the schema;
20456 // we want the engine to recognise these PG meta tables
20457 // and synthesise rows from the live catalog. Produce a
20458 // synthetic name (`__spg_info_columns` etc.) so the
20459 // engine's SELECT-side router can dispatch without
20460 // clashing with any user-defined `columns` table.
20461 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20462 (synth, Some(orig))
20463 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20464 (synth, Some(orig))
20465 } else {
20466 (self.expect_ident_like()?, None)
20467 };
20468 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20469 // time-travel clause. Parse BEFORE the alias so the
20470 // alias can still ride at the tail (`tbl AS OF SEGMENT
20471 // '5' alias`). `AS` is a reserved keyword token, while
20472 // `OF` and `SEGMENT` are bare idents.
20473 let as_of_segment = if matches!(self.peek(), Token::As)
20474 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20475 {
20476 self.advance(); // AS
20477 self.advance(); // OF
20478 let kw = match self.peek().clone() {
20479 Token::Ident(s) | Token::QuotedIdent(s) => s,
20480 other => {
20481 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20482 }
20483 };
20484 if !kw.eq_ignore_ascii_case("segment") {
20485 return Err(self.err(format!(
20486 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20487 )));
20488 }
20489 self.advance();
20490 // Segment id literal — accept either a string or
20491 // integer for operator ergonomics.
20492 let id = match self.advance() {
20493 Token::String(s) => s
20494 .parse::<u32>()
20495 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20496 Token::Integer(n) => u32::try_from(n)
20497 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20498 other => {
20499 return Err(self.err(format!(
20500 "expected segment id literal after AS OF SEGMENT, got {other:?}"
20501 )));
20502 }
20503 };
20504 Some(id)
20505 } else {
20506 None
20507 };
20508 // TABLESAMPLE is not a reserved token — keep the bare-ident
20509 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20510 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20511 {
20512 None
20513 } else {
20514 self.parse_optional_alias()?
20515 };
20516 // r1052 — a catalog name rewritten to its synthetic form keeps
20517 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20518 // still binds after `pg_cast` became `__spg_pg_cast`. PG
20519 // semantics: the visible name of `pg_catalog.pg_cast` IS
20520 // `pg_cast`. Without this, every table-name-qualified column
20521 // on a synthesised catalog answered "missing FROM-clause
20522 // entry" — which is the wall pg_dump hit on its first
20523 // pg_proc/pg_cast query.
20524 let alias = match (&alias, &meta_original) {
20525 (None, Some(orig)) if *orig != name => Some(orig.clone()),
20526 _ => alias,
20527 };
20528 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20529 // (PG grammar). BERNOULLI lowers to a per-row
20530 // `random() < p/100` conjunct on the enclosing SELECT's
20531 // WHERE — exact row-level Bernoulli semantics. SYSTEM
20532 // shares the lowering: SPG has no page structure to
20533 // sample, and the row-level form returns the same expected
20534 // fraction. REPEATABLE(seed) promises a deterministic
20535 // sample SPG cannot honour yet — honest error rather than
20536 // a silently ignored seed.
20537 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20538 self.advance();
20539 let method = self.expect_ident_like()?;
20540 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20541 return Err(self.err(alloc::format!(
20542 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20543 )));
20544 }
20545 if !matches!(self.peek(), Token::LParen) {
20546 return Err(self.err(alloc::format!(
20547 "expected '(' after TABLESAMPLE {}, got {:?}",
20548 method.to_ascii_uppercase(),
20549 self.peek()
20550 )));
20551 }
20552 self.advance();
20553 let percent = self.parse_expr(0)?;
20554 if !matches!(self.peek(), Token::RParen) {
20555 return Err(self.err(alloc::format!(
20556 "expected ')' after TABLESAMPLE percentage, got {:?}",
20557 self.peek()
20558 )));
20559 }
20560 self.advance();
20561 // REPEATABLE(seed) → a deterministic per-row draw seeded by
20562 // `seed`, so the sample is stable across repeats and rescans.
20563 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20564 let mut sample_seed: Option<Expr> = None;
20565 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20566 self.advance();
20567 if !matches!(self.peek(), Token::LParen) {
20568 return Err(self.err(alloc::format!(
20569 "expected '(' after REPEATABLE, got {:?}",
20570 self.peek()
20571 )));
20572 }
20573 self.advance();
20574 let seed = self.parse_expr(0)?;
20575 if !matches!(self.peek(), Token::RParen) {
20576 return Err(self.err(alloc::format!(
20577 "expected ')' after REPEATABLE seed, got {:?}",
20578 self.peek()
20579 )));
20580 }
20581 self.advance();
20582 sample_seed = Some(seed);
20583 }
20584 let draw = match sample_seed {
20585 Some(seed) => Expr::FunctionCall {
20586 name: "__tsm_fract".to_string(),
20587 args: alloc::vec![seed],
20588 },
20589 None => Expr::FunctionCall {
20590 name: "random".to_string(),
20591 args: Vec::new(),
20592 },
20593 };
20594 self.pending_sample_preds.push(Expr::Binary {
20595 lhs: Box::new(draw),
20596 op: crate::ast::BinOp::Lt,
20597 rhs: Box::new(Expr::Binary {
20598 lhs: Box::new(percent),
20599 op: crate::ast::BinOp::Div,
20600 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20601 }),
20602 });
20603 }
20604 Ok(TableRef {
20605 name,
20606 alias,
20607 only,
20608 as_of_segment,
20609 unnest_expr: None,
20610 unnest_column_aliases: Vec::new(),
20611 with_ordinality: false,
20612 generate_series_args: None,
20613 lateral_subquery: None,
20614 jsonb_each_text_arg: None,
20615 table_fn_call: None,
20616 rows_from: None,
20617 json_table: None,
20618 scalar_fn_item: false,
20619 })
20620 }
20621
20622 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20623 /// but also accepts `AS alias(col [, col, …])` — the
20624 /// PG-standard table-function column-list form. The column
20625 /// list is only honoured when paired with `UNNEST(...)` in
20626 /// the parent; other call sites currently discard it.
20627 /// True when the expression tree contains a qualified column
20628 /// reference (`t.col`) — the syntactic marker that an SRF
20629 /// argument correlates with a preceding FROM item.
20630 fn expr_has_qualified_column(e: &Expr) -> bool {
20631 match e {
20632 Expr::Column(c) => c.qualifier.is_some(),
20633 Expr::Binary { lhs, rhs, .. } => {
20634 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20635 }
20636 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20637 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20638 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20639 Expr::Case {
20640 operand,
20641 branches,
20642 else_branch,
20643 } => {
20644 operand
20645 .as_deref()
20646 .is_some_and(Self::expr_has_qualified_column)
20647 || branches.iter().any(|(w, t)| {
20648 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20649 })
20650 || else_branch
20651 .as_deref()
20652 .is_some_and(Self::expr_has_qualified_column)
20653 }
20654 _ => false,
20655 }
20656 }
20657
20658 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20659 /// counts a bare (unqualified) column. A set-returning function has no
20660 /// input columns of its own, so ANY column in its arguments is an outer
20661 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20662 fn expr_has_any_column(e: &Expr) -> bool {
20663 match e {
20664 Expr::Column(_) => true,
20665 Expr::Binary { lhs, rhs, .. } => {
20666 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20667 }
20668 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20669 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20670 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20671 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20672 // constructor or subscript fell to the `_ => false` arm, so
20673 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20674 // channel and the eager peer eval answered `column "x" does
20675 // not exist` (the substitution walker already recurses both
20676 // shapes; only this detector was blind to them).
20677 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20678 Expr::ArraySubscript { target, index } => {
20679 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20680 }
20681 Expr::Case {
20682 operand,
20683 branches,
20684 else_branch,
20685 } => {
20686 operand.as_deref().is_some_and(Self::expr_has_any_column)
20687 || branches
20688 .iter()
20689 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20690 || else_branch
20691 .as_deref()
20692 .is_some_and(Self::expr_has_any_column)
20693 }
20694 _ => false,
20695 }
20696 }
20697
20698 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20699 /// `generate_series(1, t.n)`) into the lateral_subquery
20700 /// channel: `SELECT * FROM <srf>` executes per outer row with
20701 /// outer references substituted (v7.37.43-T4.5 machinery).
20702 /// Uncorrelated SRFs stay on their plain channels.
20703 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20704 let name = srf.name.clone();
20705 let alias = srf.alias.clone();
20706 let inner = crate::ast::SelectStatement {
20707 locking: None,
20708 ctes: Vec::new(),
20709 distinct: false,
20710 distinct_on: Vec::new(),
20711 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20712 from: Some(crate::ast::FromClause {
20713 primary: srf,
20714 joins: Vec::new(),
20715 }),
20716 where_: None,
20717 group_by: None,
20718 group_by_all: false,
20719 having: None,
20720 unions: Vec::new(),
20721 order_by: Vec::new(),
20722 limit: None,
20723 offset: None,
20724 limit_with_ties: false,
20725 window_check_exprs: Vec::new(),
20726 };
20727 TableRef {
20728 name,
20729 alias,
20730 only: false,
20731 as_of_segment: None,
20732 unnest_expr: None,
20733 unnest_column_aliases: Vec::new(),
20734 with_ordinality: false,
20735 generate_series_args: None,
20736 lateral_subquery: Some(Box::new(inner)),
20737 jsonb_each_text_arg: None,
20738 table_fn_call: None,
20739 rows_from: None,
20740 json_table: None,
20741 scalar_fn_item: false,
20742 }
20743 }
20744
20745 /// True when the expression tree contains an unresolved
20746 /// `OVER w` marker (see parse_over_clause).
20747 fn expr_has_named_window(e: &Expr) -> bool {
20748 match e {
20749 Expr::WindowFunction { partition_by, .. } => matches!(
20750 partition_by.as_slice(),
20751 [Expr::Column(c)] if matches!(
20752 c.qualifier.as_deref(),
20753 Some("__named_window__") | Some("__named_window_ref__")
20754 )
20755 ),
20756 Expr::Binary { lhs, rhs, .. } => {
20757 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20758 }
20759 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20760 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20761 Expr::Case {
20762 operand,
20763 branches,
20764 else_branch,
20765 } => {
20766 operand.as_deref().is_some_and(Self::expr_has_named_window)
20767 || branches.iter().any(|(w, t)| {
20768 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20769 })
20770 || else_branch
20771 .as_deref()
20772 .is_some_and(Self::expr_has_named_window)
20773 }
20774 _ => false,
20775 }
20776 }
20777
20778 /// v7.39 (round 705) — the NAMES the expression references through the
20779 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20780 /// definitions nothing referenced. Traversal mirrors
20781 /// `expr_has_named_window` above.
20782 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20783 match e {
20784 Expr::WindowFunction { partition_by, .. } => {
20785 if let [Expr::Column(c)] = partition_by.as_slice()
20786 && matches!(
20787 c.qualifier.as_deref(),
20788 Some("__named_window__") | Some("__named_window_ref__")
20789 )
20790 {
20791 into.push(c.name.clone());
20792 }
20793 }
20794 Expr::Binary { lhs, rhs, .. } => {
20795 Self::collect_named_window_refs(lhs, into);
20796 Self::collect_named_window_refs(rhs, into);
20797 }
20798 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20799 Self::collect_named_window_refs(expr, into);
20800 }
20801 Expr::FunctionCall { args, .. } => {
20802 for a in args {
20803 Self::collect_named_window_refs(a, into);
20804 }
20805 }
20806 Expr::Case {
20807 operand,
20808 branches,
20809 else_branch,
20810 } => {
20811 if let Some(o) = operand.as_deref() {
20812 Self::collect_named_window_refs(o, into);
20813 }
20814 for (w, t) in branches {
20815 Self::collect_named_window_refs(w, into);
20816 Self::collect_named_window_refs(t, into);
20817 }
20818 if let Some(eb) = else_branch.as_deref() {
20819 Self::collect_named_window_refs(eb, into);
20820 }
20821 }
20822 _ => {}
20823 }
20824 }
20825
20826 /// Inline named-window definitions into the `OVER w` markers.
20827 /// An unknown name errors (PG: window "w" does not exist).
20828 #[allow(clippy::type_complexity)]
20829 fn substitute_named_windows(
20830 e: &mut Expr,
20831 defs: &[(
20832 String,
20833 (
20834 Vec<Expr>,
20835 Vec<(Expr, bool, Option<bool>)>,
20836 Option<WindowFrame>,
20837 ),
20838 )],
20839 ) -> Result<(), String> {
20840 match e {
20841 Expr::WindowFunction {
20842 partition_by,
20843 order_by,
20844 frame,
20845 ..
20846 } => {
20847 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20848 // from the bare `OVER w1` (a plain reference).
20849 let named = match partition_by.as_slice() {
20850 [Expr::Column(c)] => match c.qualifier.as_deref() {
20851 Some("__named_window__") => Some((c.name.clone(), false)),
20852 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20853 _ => None,
20854 },
20855 _ => None,
20856 };
20857 if let Some((wname, is_copy)) = named {
20858 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20859 else {
20860 return Err(alloc::format!("window {wname:?} does not exist"));
20861 };
20862 if !is_copy {
20863 *partition_by = def.0.clone();
20864 *order_by = def.1.clone();
20865 *frame = def.2.clone();
20866 return Ok(());
20867 }
20868 // v7.39 (round 229) — PG's copy rules, probed against
20869 // 18.4: a copy inherits the partitioning, may supply an
20870 // ordering only when the base has none, and may not copy
20871 // a base that already carries a frame (its own frame
20872 // would be ambiguous with the inherited one).
20873 if !def.1.is_empty() && !order_by.is_empty() {
20874 return Err(alloc::format!(
20875 "cannot override ORDER BY clause of window \"{wname}\""
20876 ));
20877 }
20878 if def.2.is_some() {
20879 return Err(alloc::format!(
20880 "cannot copy window \"{wname}\" because it has a frame clause"
20881 ));
20882 }
20883 *partition_by = def.0.clone();
20884 if order_by.is_empty() {
20885 *order_by = def.1.clone();
20886 }
20887 }
20888 Ok(())
20889 }
20890 Expr::Binary { lhs, rhs, .. } => {
20891 Self::substitute_named_windows(lhs, defs)?;
20892 Self::substitute_named_windows(rhs, defs)
20893 }
20894 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20895 Self::substitute_named_windows(expr, defs)
20896 }
20897 Expr::FunctionCall { args, .. } => {
20898 for a in args {
20899 Self::substitute_named_windows(a, defs)?;
20900 }
20901 Ok(())
20902 }
20903 Expr::Case {
20904 operand,
20905 branches,
20906 else_branch,
20907 } => {
20908 if let Some(op) = operand {
20909 Self::substitute_named_windows(op, defs)?;
20910 }
20911 for (w, t) in branches {
20912 Self::substitute_named_windows(w, defs)?;
20913 Self::substitute_named_windows(t, defs)?;
20914 }
20915 if let Some(el) = else_branch {
20916 Self::substitute_named_windows(el, defs)?;
20917 }
20918 Ok(())
20919 }
20920 _ => Ok(()),
20921 }
20922 }
20923
20924 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20925 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20926 /// composition.
20927 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20928 debug_assert!(matches!(self.peek(), Token::Table));
20929 self.advance(); // TABLE
20930 let tname = self.expect_ident_like()?;
20931 Ok(SelectStatement {
20932 locking: None,
20933 ctes: Vec::new(),
20934 distinct: false,
20935 distinct_on: Vec::new(),
20936 items: alloc::vec![SelectItem::Wildcard],
20937 from: Some(FromClause {
20938 primary: TableRef {
20939 name: tname,
20940 alias: None,
20941 only: false,
20942 as_of_segment: None,
20943 unnest_expr: None,
20944 unnest_column_aliases: Vec::new(),
20945 with_ordinality: false,
20946 generate_series_args: None,
20947 lateral_subquery: None,
20948 jsonb_each_text_arg: None,
20949 table_fn_call: None,
20950 rows_from: None,
20951 json_table: None,
20952 scalar_fn_item: false,
20953 },
20954 joins: Vec::new(),
20955 }),
20956 where_: None,
20957 group_by: None,
20958 group_by_all: false,
20959 having: None,
20960 unions: Vec::new(),
20961 order_by: Vec::new(),
20962 limit: None,
20963 offset: None,
20964 limit_with_ties: false,
20965 window_check_exprs: Vec::new(),
20966 })
20967 }
20968
20969 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20970 /// variants) → a derived table that reads each declared column out of
20971 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20972 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20973 /// the scalar *record form projects a single row straight off `J`.
20974 /// Rides the existing lateral-subquery channel, so no new executor or
20975 /// AST is needed.
20976 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20977 use crate::ast::{
20978 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20979 };
20980 let fn_name = match self.peek() {
20981 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20982 _ => unreachable!("caller guarded is_json_to_record_name"),
20983 };
20984 self.advance(); // fn name
20985 self.advance(); // (
20986 let mut arg = self.parse_expr(0)?;
20987 // populate_record(base, json): the base only carries the record
20988 // type here — the JSON argument is the second expression.
20989 let mut base: Option<Expr> = None;
20990 if matches!(self.peek(), Token::Comma) {
20991 self.advance();
20992 base = Some(arg);
20993 arg = self.parse_expr(0)?;
20994 }
20995 if !matches!(self.peek(), Token::RParen) {
20996 return Err(self.err(alloc::format!(
20997 "expected ')' after {fn_name}() argument, got {:?}",
20998 self.peek()
20999 )));
21000 }
21001 self.advance(); // )
21002 let is_set = fn_name.ends_with("recordset");
21003 // `[AS] alias ( col type [, …] )` column-definition list.
21004 if matches!(self.peek(), Token::As) {
21005 self.advance();
21006 }
21007 let alias_opt = match self.peek() {
21008 Token::Ident(s) | Token::QuotedIdent(s) => {
21009 let a = s.clone();
21010 self.advance();
21011 Some(a)
21012 }
21013 _ => None,
21014 };
21015 // v7.39 (read01 round 76) — the populate family's canonical PG
21016 // spelling carries no column list at all: the row shape comes from
21017 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
21018 // j)`). The parser has no catalog, so hand the two arguments to the
21019 // engine's table-function channel, which does. Only `*_to_record*`
21020 // (whose base is bare `record`) genuinely requires the list.
21021 if !matches!(self.peek(), Token::LParen) {
21022 if let Some(base_expr) = base {
21023 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
21024 return Ok(TableRef {
21025 name: alias.clone(),
21026 alias: Some(alias),
21027 only: false,
21028 as_of_segment: None,
21029 unnest_expr: None,
21030 unnest_column_aliases: Vec::new(),
21031 with_ordinality: false,
21032 generate_series_args: None,
21033 lateral_subquery: None,
21034 jsonb_each_text_arg: None,
21035 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
21036 rows_from: None,
21037 json_table: None,
21038 scalar_fn_item: false,
21039 });
21040 }
21041 return Err(self.err(alloc::format!(
21042 "expected '(' to start the {fn_name} column-definition list, got {:?}",
21043 self.peek()
21044 )));
21045 }
21046 let Some(alias) = alias_opt else {
21047 return Err(self.err(alloc::format!(
21048 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
21049 )));
21050 };
21051 self.advance(); // (
21052 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
21053 loop {
21054 let col = self.expect_ident_like()?;
21055 let ty = self.parse_cast_target()?;
21056 coldefs.push((col, ty));
21057 if matches!(self.peek(), Token::Comma) {
21058 self.advance();
21059 continue;
21060 }
21061 if matches!(self.peek(), Token::RParen) {
21062 self.advance();
21063 break;
21064 }
21065 return Err(self.err(alloc::format!(
21066 "expected ',' or ')' in {fn_name} column list, got {:?}",
21067 self.peek()
21068 )));
21069 }
21070 if coldefs.is_empty() {
21071 return Err(self.err(alloc::format!(
21072 "{fn_name} column-definition list must declare at least one column"
21073 )));
21074 }
21075 // Per column: (base ->> 'col')::type AS col. The base is the
21076 // per-element `value` column for the *set form, or the argument
21077 // itself for the scalar record form.
21078 let items: Vec<SelectItem> = coldefs
21079 .into_iter()
21080 .map(|(col, ty)| {
21081 let base = if is_set {
21082 Expr::Column(ColumnName {
21083 qualifier: None,
21084 name: "value".to_string(),
21085 })
21086 } else {
21087 arg.clone()
21088 };
21089 SelectItem::Expr {
21090 expr: Expr::Cast {
21091 expr: Box::new(Expr::Binary {
21092 lhs: Box::new(base),
21093 op: BinOp::JsonGetText,
21094 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21095 }),
21096 target: ty,
21097 },
21098 alias: Some(col),
21099 }
21100 })
21101 .collect();
21102 let from = if is_set {
21103 let elem_fn = if fn_name.starts_with("jsonb") {
21104 "jsonb_array_elements"
21105 } else {
21106 "json_array_elements"
21107 };
21108 Some(FromClause {
21109 primary: TableRef {
21110 name: "value".to_string(),
21111 alias: None,
21112 only: false,
21113 as_of_segment: None,
21114 unnest_expr: Some(Box::new(Expr::FunctionCall {
21115 name: elem_fn.to_string(),
21116 args: alloc::vec![arg],
21117 })),
21118 unnest_column_aliases: alloc::vec!["value".to_string()],
21119 with_ordinality: false,
21120 generate_series_args: None,
21121 lateral_subquery: None,
21122 jsonb_each_text_arg: None,
21123 table_fn_call: None,
21124 rows_from: None,
21125 json_table: None,
21126 scalar_fn_item: false,
21127 },
21128 joins: Vec::new(),
21129 })
21130 } else {
21131 None
21132 };
21133 let inner = SelectStatement {
21134 locking: None,
21135 ctes: Vec::new(),
21136 distinct: false,
21137 distinct_on: Vec::new(),
21138 items,
21139 from,
21140 where_: None,
21141 group_by: None,
21142 group_by_all: false,
21143 having: None,
21144 unions: Vec::new(),
21145 order_by: Vec::new(),
21146 limit: None,
21147 offset: None,
21148 limit_with_ties: false,
21149 window_check_exprs: Vec::new(),
21150 };
21151 Ok(TableRef {
21152 name: alias.clone(),
21153 alias: Some(alias),
21154 only: false,
21155 as_of_segment: None,
21156 unnest_expr: None,
21157 unnest_column_aliases: Vec::new(),
21158 with_ordinality: false,
21159 generate_series_args: None,
21160 lateral_subquery: Some(Box::new(inner)),
21161 jsonb_each_text_arg: None,
21162 table_fn_call: None,
21163 rows_from: None,
21164 json_table: None,
21165 scalar_fn_item: false,
21166 })
21167 }
21168
21169 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21170 /// Returns true when the clause was present. `WITH` alone (a
21171 /// CTE can never start here) is not enough — the ORDINALITY
21172 /// ident must follow, so a stray WITH still errors downstream.
21173 fn absorb_with_ordinality(&mut self) -> bool {
21174 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21175 && matches!(self.tokens.get(self.pos + 1),
21176 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21177 {
21178 self.advance();
21179 self.advance();
21180 true
21181 } else {
21182 false
21183 }
21184 }
21185
21186 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21187 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21188 /// Out-of-line: the caller sits on the FROM recursion chain.
21189 #[inline(never)]
21190 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21191 let fn_name = match self.advance() {
21192 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21193 _ => unreachable!("caller peeked an ident"),
21194 };
21195 self.advance(); // (
21196 let mut args: Vec<Expr> = Vec::new();
21197 if !matches!(self.peek(), Token::RParen) {
21198 loop {
21199 args.push(self.parse_expr(0)?);
21200 if matches!(self.peek(), Token::Comma) {
21201 self.advance();
21202 continue;
21203 }
21204 break;
21205 }
21206 }
21207 if !matches!(self.peek(), Token::RParen) {
21208 return Err(self.err(alloc::format!(
21209 "expected ')' after {fn_name}() arguments, got {:?}",
21210 self.peek()
21211 )));
21212 }
21213 self.advance();
21214 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21215 // counter column rides after the function's own, and the alias list
21216 // names it.
21217 let with_ordinality = self.absorb_with_ordinality();
21218 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21219 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21220 Ok(TableRef {
21221 name,
21222 alias: alias_ident,
21223 only: false,
21224 as_of_segment: None,
21225 unnest_expr: None,
21226 unnest_column_aliases,
21227 with_ordinality,
21228 generate_series_args: None,
21229 lateral_subquery: None,
21230 jsonb_each_text_arg: None,
21231 table_fn_call: Some(Box::new((fn_name, args))),
21232 rows_from: None,
21233 json_table: None,
21234 scalar_fn_item: false,
21235 })
21236 }
21237
21238 /// v7.39 (round 205, JSON_TABLE) — parse
21239 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21240 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21241 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21242 #[inline(never)]
21243 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21244 self.advance(); // json_table
21245 self.advance(); // (
21246 let doc = Box::new(self.parse_expr(0)?);
21247 self.expect_comma_json_table()?;
21248 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21249 // Optional `PASSING <expr> AS <name> [, …]`.
21250 let mut passing: Vec<(String, Expr)> = Vec::new();
21251 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21252 self.advance();
21253 loop {
21254 let e = self.parse_expr(0)?;
21255 if !matches!(self.peek(), Token::As) {
21256 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21257 }
21258 self.advance();
21259 let vname = match self.advance() {
21260 Token::Ident(s) | Token::QuotedIdent(s) => s,
21261 other => {
21262 return Err(self.err(alloc::format!(
21263 "expected PASSING variable name, got {other:?}"
21264 )));
21265 }
21266 };
21267 passing.push((vname, e));
21268 if matches!(self.peek(), Token::Comma) {
21269 self.advance();
21270 continue;
21271 }
21272 break;
21273 }
21274 }
21275 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21276 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21277 }
21278 self.advance();
21279 let columns = self.parse_json_table_columns()?;
21280 if !matches!(self.peek(), Token::RParen) {
21281 return Err(self.err(alloc::format!(
21282 "expected ')' to close JSON_TABLE, got {:?}",
21283 self.peek()
21284 )));
21285 }
21286 self.advance();
21287 let alias_ident = self.parse_optional_alias()?;
21288 let name = alias_ident
21289 .clone()
21290 .unwrap_or_else(|| String::from("json_table"));
21291 Ok(TableRef {
21292 name,
21293 alias: alias_ident,
21294 only: false,
21295 as_of_segment: None,
21296 unnest_expr: None,
21297 unnest_column_aliases: Vec::new(),
21298 with_ordinality: false,
21299 generate_series_args: None,
21300 lateral_subquery: None,
21301 jsonb_each_text_arg: None,
21302 table_fn_call: None,
21303 rows_from: None,
21304 json_table: Some(Box::new(crate::ast::JsonTable {
21305 doc,
21306 row_path,
21307 columns,
21308 passing,
21309 })),
21310 scalar_fn_item: false,
21311 })
21312 }
21313
21314 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21315 if !matches!(self.peek(), Token::Comma) {
21316 return Err(self.err(alloc::format!(
21317 "expected ',' after JSON_TABLE document, got {:?}",
21318 self.peek()
21319 )));
21320 }
21321 self.advance();
21322 Ok(())
21323 }
21324
21325 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21326 match self.advance() {
21327 Token::String(s) => Ok(s),
21328 other => Err(self.err(alloc::format!(
21329 "expected {what} string literal, got {other:?}"
21330 ))),
21331 }
21332 }
21333
21334 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21335 #[inline(never)]
21336 fn parse_json_table_columns(
21337 &mut self,
21338 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21339 if !matches!(self.peek(), Token::LParen) {
21340 return Err(self.err("expected '(' after COLUMNS".into()));
21341 }
21342 self.advance();
21343 let mut cols = Vec::new();
21344 loop {
21345 cols.push(self.parse_json_table_one_column()?);
21346 if matches!(self.peek(), Token::Comma) {
21347 self.advance();
21348 continue;
21349 }
21350 break;
21351 }
21352 if !matches!(self.peek(), Token::RParen) {
21353 return Err(self.err(alloc::format!(
21354 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21355 self.peek()
21356 )));
21357 }
21358 self.advance();
21359 Ok(cols)
21360 }
21361
21362 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21363 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21364 // NESTED [PATH] '<p>' COLUMNS (...)
21365 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21366 self.advance();
21367 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21368 self.advance();
21369 }
21370 let path = self.parse_json_string_literal("NESTED PATH")?;
21371 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21372 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21373 }
21374 self.advance();
21375 let columns = self.parse_json_table_columns()?;
21376 return Ok(JsonTableColumn::Nested { path, columns });
21377 }
21378 // <name> ...
21379 let name = match self.advance() {
21380 Token::Ident(s) | Token::QuotedIdent(s) => s,
21381 other => {
21382 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21383 }
21384 };
21385 // <name> FOR ORDINALITY
21386 if matches!(self.peek(), Token::For) {
21387 self.advance();
21388 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21389 return Err(self.err("expected ORDINALITY after FOR".into()));
21390 }
21391 self.advance();
21392 return Ok(JsonTableColumn::Ordinality { name });
21393 }
21394 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21395 let ty = self.parse_column_type_name()?;
21396 let mut format_json = false;
21397 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21398 self.advance();
21399 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21400 return Err(self.err("expected JSON after FORMAT".into()));
21401 }
21402 self.advance();
21403 format_json = true;
21404 }
21405 let mut exists = false;
21406 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21407 self.advance();
21408 exists = true;
21409 }
21410 let mut path = alloc::format!("$.{name}");
21411 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21412 self.advance();
21413 path = self.parse_json_string_literal("column PATH")?;
21414 }
21415 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21416 // `FORMAT JSON` after PATH (alternate placement).
21417 self.advance();
21418 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21419 self.advance();
21420 }
21421 format_json = true;
21422 }
21423 let mut wrapper = false;
21424 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21425 self.advance();
21426 // optional CONDITIONAL/UNCONDITIONAL
21427 if matches!(self.peek(), Token::Ident(s)
21428 if s.eq_ignore_ascii_case("unconditional")
21429 || s.eq_ignore_ascii_case("conditional"))
21430 {
21431 self.advance();
21432 }
21433 if !matches!(self.peek(), Token::Ident(s)
21434 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21435 {
21436 return Err(self.err("expected WRAPPER after WITH".into()));
21437 }
21438 self.advance();
21439 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21440 if matches!(self.peek(), Token::Ident(s)
21441 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21442 {
21443 self.advance();
21444 }
21445 wrapper = true;
21446 }
21447 // ON EMPTY / ON ERROR clauses (two, in any order).
21448 let mut on_empty = JsonTableOnBehavior::Null;
21449 let mut on_error = JsonTableOnBehavior::Null;
21450 for _ in 0..2 {
21451 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21452 {
21453 self.advance();
21454 Some(JsonTableOnBehavior::Error)
21455 } else if matches!(self.peek(), Token::Null) {
21456 self.advance();
21457 Some(JsonTableOnBehavior::Null)
21458 } else if matches!(self.peek(), Token::Default) {
21459 self.advance();
21460 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21461 } else {
21462 None
21463 };
21464 let Some(behavior) = behavior else { break };
21465 // `ON {EMPTY|ERROR}`
21466 if !matches!(self.peek(), Token::On) {
21467 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21468 }
21469 self.advance();
21470 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21471 self.advance();
21472 on_empty = behavior;
21473 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21474 self.advance();
21475 on_error = behavior;
21476 } else {
21477 return Err(self.err("expected EMPTY or ERROR after ON".into()));
21478 }
21479 }
21480 Ok(JsonTableColumn::Regular {
21481 name,
21482 ty,
21483 path,
21484 exists,
21485 format_json,
21486 wrapper,
21487 on_empty,
21488 on_error,
21489 })
21490 }
21491
21492 fn parse_optional_alias_with_columns(
21493 &mut self,
21494 ) -> Result<(Option<String>, Vec<String>), ParseError> {
21495 let alias = self.parse_optional_alias()?;
21496 if alias.is_none() {
21497 return Ok((None, Vec::new()));
21498 }
21499 let mut cols: Vec<String> = Vec::new();
21500 if matches!(self.peek(), Token::LParen) {
21501 self.advance();
21502 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21503 self.advance();
21504 cols.push(s);
21505 if matches!(self.peek(), Token::Comma) {
21506 self.advance();
21507 continue;
21508 }
21509 break;
21510 }
21511 if matches!(self.peek(), Token::RParen) {
21512 self.advance();
21513 }
21514 }
21515 Ok((alias, cols))
21516 }
21517
21518 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21519 /// whose keyword token was already consumed and whose `(` is the
21520 /// current token. Factored out of `parse_atom` (and marked
21521 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21522 /// recursive `parse_atom` frame — inlining them there enlarges the
21523 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21524 /// against, risking an overflow before the budget triggers.
21525 #[inline(never)]
21526 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21527 self.advance(); // (
21528 let mut args = Vec::new();
21529 if !matches!(self.peek(), Token::RParen) {
21530 loop {
21531 args.push(self.parse_expr(0)?);
21532 match self.peek() {
21533 Token::Comma => {
21534 self.advance();
21535 }
21536 Token::RParen => break,
21537 other => {
21538 return Err(self.err(alloc::format!(
21539 "expected ',' or ')' in {name}() args, got {other:?}"
21540 )));
21541 }
21542 }
21543 }
21544 }
21545 self.advance(); // )
21546 Ok(Expr::FunctionCall {
21547 name: name.into(),
21548 args,
21549 })
21550 }
21551
21552 /// FROM-clause: a primary table reference plus zero-or-more joined
21553 /// peers expressed via either `, <table>` (cross-product, no ON) or
21554 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21555 /// v1.10 keeps the join list flat (left-associative nested-loop
21556 /// semantics).
21557 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21558 let primary = self.parse_table_ref()?;
21559 let primary_qual = primary
21560 .alias
21561 .clone()
21562 .unwrap_or_else(|| primary.name.clone());
21563 let joins = self.parse_from_joins(&primary_qual)?;
21564 Ok(FromClause { primary, joins })
21565 }
21566
21567 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21568 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21569 /// SAME grammar after its target table has already been consumed.
21570 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21571 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21572 /// be parsed forward, once.)
21573 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21574 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21575 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21576 /// desugaring, which needs a name for the left side of each equality.
21577 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21578 let mut joins = Vec::new();
21579 loop {
21580 // `, <table>` — cross-product with no ON.
21581 if matches!(self.peek(), Token::Comma) {
21582 self.advance();
21583 let table = self.parse_table_ref()?;
21584 joins.push(FromJoin {
21585 kind: JoinKind::Cross,
21586 table,
21587 on: None,
21588 using_cols: None,
21589 natural: false,
21590 });
21591 continue;
21592 }
21593 // v7.37.16 — optional leading `NATURAL` before the join
21594 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21595 // not a lexer keyword (it arrives as a bare Ident), so match
21596 // it case-insensitively here. When present, no ON/USING
21597 // clause is allowed — the common columns are resolved at
21598 // execution time.
21599 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21600 if natural {
21601 self.advance();
21602 }
21603 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21604 // CROSS JOIN, and bare JOIN (defaults to INNER).
21605 let kind =
21606 match self.peek() {
21607 Token::Inner => {
21608 self.advance();
21609 if !matches!(self.peek(), Token::Join) {
21610 return Err(self
21611 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21612 }
21613 self.advance();
21614 JoinKind::Inner
21615 }
21616 Token::Left => {
21617 self.advance();
21618 if matches!(self.peek(), Token::Outer) {
21619 self.advance();
21620 }
21621 if !matches!(self.peek(), Token::Join) {
21622 return Err(self.err(format!(
21623 "expected JOIN after LEFT [OUTER], got {:?}",
21624 self.peek()
21625 )));
21626 }
21627 self.advance();
21628 JoinKind::Left
21629 }
21630 Token::Cross => {
21631 self.advance();
21632 if !matches!(self.peek(), Token::Join) {
21633 return Err(self
21634 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21635 }
21636 self.advance();
21637 JoinKind::Cross
21638 }
21639 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21640 Token::Right => {
21641 self.advance();
21642 if matches!(self.peek(), Token::Outer) {
21643 self.advance();
21644 }
21645 if !matches!(self.peek(), Token::Join) {
21646 return Err(self.err(format!(
21647 "expected JOIN after RIGHT [OUTER], got {:?}",
21648 self.peek()
21649 )));
21650 }
21651 self.advance();
21652 JoinKind::Right
21653 }
21654 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21655 Token::Full => {
21656 self.advance();
21657 if matches!(self.peek(), Token::Outer) {
21658 self.advance();
21659 }
21660 if !matches!(self.peek(), Token::Join) {
21661 return Err(self.err(format!(
21662 "expected JOIN after FULL [OUTER], got {:?}",
21663 self.peek()
21664 )));
21665 }
21666 self.advance();
21667 JoinKind::FullOuter
21668 }
21669 Token::Join => {
21670 self.advance();
21671 JoinKind::Inner
21672 }
21673 _ => break,
21674 };
21675 let table = self.parse_table_ref()?;
21676 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21677 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21678 // where prev_table is the most-recent left-side table
21679 // (the previous join's table if any, else the FROM primary).
21680 // PG semantics around column merging are richer (USING'd
21681 // cols become deduplicated single output columns); for
21682 // sugar purposes the predicate-only form covers the
21683 // baseline corpus shape and chained `… JOIN x USING (k)
21684 // JOIN y USING (k)` calls.
21685 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21686 // common columns resolve at execution time.
21687 if natural {
21688 joins.push(FromJoin {
21689 kind,
21690 table,
21691 on: None,
21692 using_cols: None,
21693 natural: true,
21694 });
21695 continue;
21696 }
21697 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21698 // v7.37.16 — capture the USING column list (in addition to
21699 // the ON desugar below) so the executor can perform PG's
21700 // column-merge on the output side.
21701 let mut using_cols: Option<Vec<String>> = None;
21702 let on = if matches!(self.peek(), Token::On) {
21703 self.advance();
21704 Some(self.parse_expr(0)?)
21705 } else if using_match {
21706 self.advance();
21707 if !matches!(self.peek(), Token::LParen) {
21708 return Err(
21709 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21710 );
21711 }
21712 self.advance();
21713 let mut cols: Vec<String> = Vec::new();
21714 loop {
21715 match self.peek().clone() {
21716 Token::Ident(s) | Token::QuotedIdent(s) => {
21717 self.advance();
21718 cols.push(s);
21719 }
21720 other => {
21721 return Err(self.err(format!(
21722 "expected column name inside USING (…), got {other:?}"
21723 )));
21724 }
21725 }
21726 match self.peek() {
21727 Token::Comma => {
21728 self.advance();
21729 continue;
21730 }
21731 Token::RParen => {
21732 self.advance();
21733 break;
21734 }
21735 other => {
21736 return Err(self.err(format!(
21737 "expected ',' or ')' inside USING (…), got {other:?}"
21738 )));
21739 }
21740 }
21741 }
21742 if cols.is_empty() {
21743 return Err(self.err("USING (…) requires at least one column".to_string()));
21744 }
21745 using_cols = Some(cols.clone());
21746 // Pick the left-side alias: prev join's table if any,
21747 // else FROM primary. Use alias when present, else
21748 // table name (PG-equivalent qualifier).
21749 let left_qual: String = joins
21750 .last()
21751 .map(|j| {
21752 j.table
21753 .alias
21754 .clone()
21755 .unwrap_or_else(|| j.table.name.clone())
21756 })
21757 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21758 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21759 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21760 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21761 qualifier: Some(left_qual.clone()),
21762 name: c.clone(),
21763 })),
21764 op: crate::ast::BinOp::Eq,
21765 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21766 qualifier: Some(right_qual.clone()),
21767 name: c,
21768 })),
21769 });
21770 let first = iter.next().expect("at least one col");
21771 Some(iter.fold(first, |acc, pred| Expr::Binary {
21772 lhs: alloc::boxed::Box::new(acc),
21773 op: crate::ast::BinOp::And,
21774 rhs: alloc::boxed::Box::new(pred),
21775 }))
21776 } else if kind == JoinKind::Cross {
21777 None
21778 } else {
21779 return Err(self.err(format!(
21780 "expected ON or USING after {:?} JOIN, got {:?}",
21781 kind,
21782 self.peek()
21783 )));
21784 };
21785 joins.push(FromJoin {
21786 kind,
21787 table,
21788 on,
21789 using_cols,
21790 natural: false,
21791 });
21792 }
21793 Ok(joins)
21794 }
21795
21796 /// Optional alias after an expression or table:
21797 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21798 /// accepted (PG-style implicit alias). Returns `None` if the next token
21799 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21800 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21801 if matches!(self.peek(), Token::As) {
21802 self.advance();
21803 // v7.39 (round 340, V56) — after AS the next token MUST be an
21804 // identifier. This used to return None and "let the caller
21805 // surface the error on the next expectation", but when AS is
21806 // the LAST token there is no next expectation: `SELECT 1 AS`
21807 // parsed clean and silently dropped the alias. PG rejects it.
21808 // v7.40.11 — a keyword is a legal alias after AS.
21809 //
21810 // `expect_ident_like` has known the unreserved class since
21811 // v7.17, and this guard never let a keyword token reach it.
21812 // So every one of them was a syntax error in alias position
21813 // while being accepted as a column name in the same build:
21814 //
21815 // CREATE TABLE rk (release int) accepted
21816 // SELECT release FROM rk accepted
21817 // SELECT 1 AS release syntax error
21818 //
21819 // Reported against 7.40.9 for `release` and `savepoint` —
21820 // two statements in a shipped subcommand of the reporter's
21821 // could not be parsed — and it is the whole class, `show`
21822 // and `index` included.
21823 //
21824 // Measured on PG 18.6: after AS, EVERY keyword is a legal
21825 // label, `limit` and `between` included. This accepts the
21826 // ones this parser can name, which is the unreserved class;
21827 // a reserved keyword after AS is still refused here and PG
21828 // takes it.
21829 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21830 return self.expect_ident_like().map(Some);
21831 }
21832 if unreserved_keyword_text(self.peek()).is_some() {
21833 return self.expect_ident_like().map(Some);
21834 }
21835 return Err(self.err(alloc::format!(
21836 "expected an alias after AS, got {:?}",
21837 self.peek()
21838 )));
21839 }
21840 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21841 // grammar reserves a long list of follow-keywords from the
21842 // alias slot. SPG's bareword approximation: skip a small
21843 // set of idents that would otherwise be swallowed as the
21844 // table alias and break trailing clauses like CREATE
21845 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21846 // CONFLICT WHERE shapes.
21847 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21848 if is_alias_stopword(s) {
21849 return Ok(None);
21850 }
21851 return Ok(self.expect_ident_like().ok());
21852 }
21853 // v7.40.11 — and a keyword, WITHOUT `AS`, which PG also takes:
21854 // `SELECT 1 release` answers 1 there.
21855 //
21856 // Not the ones that begin a trailing clause. PG reserves those
21857 // for exactly this reason and so must this: measured on PG 18.6,
21858 // `SELECT 1 limit` is `syntax error at end of input` — it read
21859 // `limit` as the clause, not as a label. Swallowing it here
21860 // would turn `SELECT 1 limit 2` into a two-token nonsense.
21861 if !matches!(self.peek(), Token::Limit | Token::Offset)
21862 && unreserved_keyword_text(self.peek()).is_some()
21863 {
21864 return Ok(self.expect_ident_like().ok());
21865 }
21866 Ok(None)
21867 }
21868
21869 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21870 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21871 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21872 // error beats a stack overflow (an overflow aborts the
21873 // embedding host process).
21874 self.enter_nested()?;
21875 let r = self.parse_expr_inner(min_prec);
21876 self.nest_depth -= 1;
21877 r
21878 }
21879
21880 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21881 /// When the upcoming tokens form one, return the underlying
21882 /// operator token and the position just past the closing paren
21883 /// so the binary loop can dispatch on the plain operator.
21884 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21885 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21886 return None;
21887 }
21888 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21889 return None;
21890 }
21891 let mut i = self.pos + 2;
21892 // Optional schema qualifier (pg_catalog.<op> etc.).
21893 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21894 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21895 {
21896 i += 2;
21897 }
21898 let op_tok = self.tokens.get(i)?.clone();
21899 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21900 return None;
21901 }
21902 Some((i + 2, op_tok))
21903 }
21904
21905 /// PG operator symbols that lower onto function calls in
21906 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21907 /// family → regexp_like, comparison rung), `^@` (starts_with,
21908 /// comparison rung), `^` (power, tighter than `*`), `#`
21909 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21910 /// subset of the OR bits so the subtraction never borrows).
21911 fn try_symbol_operator(
21912 &mut self,
21913 lhs: &Expr,
21914 min_prec: u8,
21915 ) -> Result<Option<Expr>, ParseError> {
21916 enum Sym {
21917 Regex { ci: bool, negated: bool },
21918 Like { ci: bool, negated: bool },
21919 StartsWith,
21920 Power,
21921 Xor,
21922 RangeAdjacent,
21923 }
21924 // v7.39 (IS-precedence knife) — the low-precedence postfix
21925 // predicates ride this existing leaf call (zero new frame slots
21926 // on the nesting chain).
21927 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21928 return Ok(Some(e));
21929 }
21930 let (sym, prec): (Sym, u8) = match self.peek() {
21931 Token::Tilde => (
21932 Sym::Regex {
21933 ci: false,
21934 negated: false,
21935 },
21936 5,
21937 ),
21938 Token::TildeStar => (
21939 Sym::Regex {
21940 ci: true,
21941 negated: false,
21942 },
21943 5,
21944 ),
21945 Token::NotTilde => (
21946 Sym::Regex {
21947 ci: false,
21948 negated: true,
21949 },
21950 5,
21951 ),
21952 Token::NotTildeStar => (
21953 Sym::Regex {
21954 ci: true,
21955 negated: true,
21956 },
21957 5,
21958 ),
21959 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21960 Token::DoubleTilde => (
21961 Sym::Like {
21962 ci: false,
21963 negated: false,
21964 },
21965 5,
21966 ),
21967 Token::DoubleTildeStar => (
21968 Sym::Like {
21969 ci: true,
21970 negated: false,
21971 },
21972 5,
21973 ),
21974 Token::NotDoubleTilde => (
21975 Sym::Like {
21976 ci: false,
21977 negated: true,
21978 },
21979 5,
21980 ),
21981 Token::NotDoubleTildeStar => (
21982 Sym::Like {
21983 ci: true,
21984 negated: true,
21985 },
21986 5,
21987 ),
21988 Token::CaretAt => (Sym::StartsWith, 5),
21989 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21990 // tighter than `* / & |`, which the prec-9 rung preserves —
21991 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21992 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21993 Token::Caret => (Sym::Power, 9),
21994 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21995 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21996 Token::Hash => (Sym::Xor, 6),
21997 Token::Adjacent => (Sym::RangeAdjacent, 5),
21998 _ => return Ok(None),
21999 };
22000 if prec < min_prec {
22001 return Ok(None);
22002 }
22003 self.advance();
22004 let rhs = self.parse_expr(prec + 1)?;
22005 let out = match sym {
22006 Sym::Regex { ci, negated } => {
22007 let mut args = alloc::vec![lhs.clone(), rhs];
22008 if ci {
22009 args.push(Expr::Literal(Literal::String(String::from("i"))));
22010 }
22011 maybe_not(
22012 Expr::FunctionCall {
22013 name: String::from("regexp_like"),
22014 args,
22015 },
22016 negated,
22017 )
22018 }
22019 Sym::Like { ci, negated } => Expr::Like {
22020 expr: alloc::boxed::Box::new(lhs.clone()),
22021 pattern: alloc::boxed::Box::new(rhs),
22022 negated,
22023 case_insensitive: ci,
22024 },
22025 Sym::StartsWith => Expr::FunctionCall {
22026 name: String::from("starts_with"),
22027 args: alloc::vec![lhs.clone(), rhs],
22028 },
22029 Sym::Power => Expr::FunctionCall {
22030 name: String::from("power"),
22031 args: alloc::vec![lhs.clone(), rhs],
22032 },
22033 // `#` bitwise XOR — a real operator now (was desugared to
22034 // `(a|b)-(a&b)`, algebraically identical for integers but
22035 // undefined for bit strings; the direct op handles both).
22036 Sym::Xor => Expr::Binary {
22037 lhs: Box::new(lhs.clone()),
22038 op: BinOp::BitXor,
22039 rhs: Box::new(rhs),
22040 },
22041 // range `-|-` "is adjacent to" — lowered to a catalog function.
22042 Sym::RangeAdjacent => Expr::FunctionCall {
22043 name: String::from("range_adjacent"),
22044 args: alloc::vec![lhs.clone(), rhs],
22045 },
22046 };
22047 Ok(Some(out))
22048 }
22049
22050 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
22051 /// predicates, moved out of the tight postfix-cast loop: PG binds
22052 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
22053 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
22054 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
22055 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
22056 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
22057 /// when nothing at this position belongs to the family. Out-of-line
22058 /// (`inline(never)`): the caller sits on the per-nesting-level frame
22059 /// chain that MAX_NEST_DEPTH is tuned against.
22060 #[inline(never)]
22061 fn parse_postfix_predicate(
22062 &mut self,
22063 lhs: &Expr,
22064 min_prec: u8,
22065 ) -> Result<Option<Expr>, ParseError> {
22066 // Reached through try_symbol_operator (an existing leaf call of
22067 // the binary loop) so NO new stack slots land on the per-nesting
22068 // frame chain; the lhs clones only when a predicate actually
22069 // consumes it.
22070 match self.peek() {
22071 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
22072 // comparison family rung 5 (each +1 from the pre-XOR ladder).
22073 Token::Is if min_prec <= 4 => {}
22074 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
22075 Token::Not
22076 if min_prec <= 5
22077 && matches!(
22078 self.tokens.get(self.pos + 1),
22079 Some(Token::Between | Token::In | Token::Like)
22080 ) => {}
22081 Token::Not | Token::Ident(_)
22082 if min_prec <= 5
22083 && (matches!(self.peek(), Token::Ident(s)
22084 if s.eq_ignore_ascii_case("ilike")
22085 || (self.mysql_dialect
22086 && (s.eq_ignore_ascii_case("regexp")
22087 || s.eq_ignore_ascii_case("rlike")))
22088 || (s.eq_ignore_ascii_case("similar")
22089 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
22090 || (matches!(self.peek(), Token::Not)
22091 && matches!(self.tokens.get(self.pos + 1),
22092 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22093 || (self.mysql_dialect
22094 && (s.eq_ignore_ascii_case("regexp")
22095 || s.eq_ignore_ascii_case("rlike")))
22096 || s.eq_ignore_ascii_case("similar")))) => {}
22097 _ => return Ok(None),
22098 }
22099 let mut expr = lhs.clone();
22100 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
22101 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
22102 if min_prec <= 4 {
22103 if matches!(self.peek(), Token::Is) {
22104 self.advance();
22105 let negated = if matches!(self.peek(), Token::Not) {
22106 self.advance();
22107 true
22108 } else {
22109 false
22110 };
22111 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
22112 // mailrs pg_dump.
22113 if matches!(self.peek(), Token::Distinct) {
22114 self.advance();
22115 if !matches!(self.peek(), Token::From) {
22116 return Err(self.err(format!(
22117 "expected FROM after IS{} DISTINCT, got {:?}",
22118 if negated { " NOT" } else { "" },
22119 self.peek()
22120 )));
22121 }
22122 self.advance();
22123 // Right-hand side: parse at the same precedence
22124 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
22125 // groups as `x IS DISTINCT FROM (a + b)`.
22126 let rhs = self.parse_expr(5)?;
22127 let op = if negated {
22128 BinOp::IsNotDistinctFrom
22129 } else {
22130 BinOp::IsDistinctFrom
22131 };
22132 expr = Expr::Binary {
22133 op,
22134 lhs: Box::new(expr),
22135 rhs: Box::new(rhs),
22136 };
22137 {
22138 return Ok(Some(expr));
22139 }
22140 }
22141 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22142 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22143 // Lowers onto pg_is_json(x, kind); NOT wraps the
22144 // call in a logical negation.
22145 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22146 if s.eq_ignore_ascii_case("json"))
22147 {
22148 self.advance(); // JSON
22149 let kind = match self.peek() {
22150 Token::Ident(s) | Token::QuotedIdent(s)
22151 if matches!(
22152 s.to_ascii_lowercase().as_str(),
22153 "value" | "object" | "array" | "scalar"
22154 ) =>
22155 {
22156 let k = s.to_ascii_lowercase();
22157 self.advance();
22158 k
22159 }
22160 _ => "value".to_string(),
22161 };
22162 let call = Expr::FunctionCall {
22163 name: "pg_is_json".to_string(),
22164 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22165 };
22166 expr = if negated {
22167 Expr::Unary {
22168 op: UnOp::Not,
22169 expr: Box::new(call),
22170 }
22171 } else {
22172 call
22173 };
22174 {
22175 return Ok(Some(expr));
22176 }
22177 }
22178 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22179 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22180 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22181 {
22182 let form_kw = match self.peek() {
22183 Token::Ident(s) | Token::QuotedIdent(s)
22184 if matches!(
22185 s.to_ascii_uppercase().as_str(),
22186 "NFC" | "NFD" | "NFKC" | "NFKD"
22187 ) && matches!(
22188 self.tokens.get(self.pos + 1),
22189 Some(Token::Ident(n) | Token::QuotedIdent(n))
22190 if n.eq_ignore_ascii_case("normalized")
22191 ) =>
22192 {
22193 Some(s.to_ascii_uppercase())
22194 }
22195 _ => None,
22196 };
22197 let bare_normalized = form_kw.is_none()
22198 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22199 if s.eq_ignore_ascii_case("normalized"));
22200 if form_kw.is_some() || bare_normalized {
22201 if form_kw.is_some() {
22202 self.advance(); // form keyword
22203 }
22204 self.advance(); // NORMALIZED
22205 let mut args = alloc::vec![expr];
22206 if let Some(f) = form_kw {
22207 args.push(Expr::Literal(Literal::String(f)));
22208 }
22209 let call = Expr::FunctionCall {
22210 name: "is_normalized".to_string(),
22211 args,
22212 };
22213 expr = if negated {
22214 Expr::Unary {
22215 op: UnOp::Not,
22216 expr: Box::new(call),
22217 }
22218 } else {
22219 call
22220 };
22221 {
22222 return Ok(Some(expr));
22223 }
22224 }
22225 }
22226 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22227 // three-valued boolean tests. IS TRUE/FALSE never
22228 // return NULL, so they lower to CASE forms whose
22229 // ELSE catches the NULL branch; IS UNKNOWN on a
22230 // boolean is exactly IS NULL.
22231 if matches!(self.peek(), Token::True | Token::False)
22232 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22233 {
22234 let tok = self.advance();
22235 let test = match tok {
22236 Token::True => Some(true),
22237 Token::False => Some(false),
22238 _ => None, // UNKNOWN
22239 };
22240 // v7.39 (round 328, V45) — kept as what the user
22241 // wrote. These used to be lowered here into `CASE` /
22242 // `IS NULL`; the semantics were right but the AST no
22243 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22244 // was echoed back as
22245 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22246 expr = Expr::BoolTest {
22247 expr: Box::new(expr),
22248 value: test,
22249 negated,
22250 };
22251 {
22252 return Ok(Some(expr));
22253 }
22254 }
22255 if !matches!(self.peek(), Token::Null) {
22256 return Err(self.err(format!(
22257 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22258 if negated { " NOT" } else { "" },
22259 self.peek()
22260 )));
22261 }
22262 self.advance();
22263 expr = Expr::IsNull {
22264 expr: Box::new(expr),
22265 negated,
22266 };
22267 {
22268 return Ok(Some(expr));
22269 }
22270 }
22271 }
22272 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22273 if min_prec <= 5 {
22274 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22275 // Look one token ahead so a stray `NOT` not followed by any of
22276 // these flows through to the early return below untouched.
22277 let negated = if matches!(self.peek(), Token::Not) {
22278 let next = self.tokens.get(self.pos + 1);
22279 matches!(next, Some(Token::Between | Token::In | Token::Like))
22280 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22281 || (self.mysql_dialect
22282 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22283 || s.eq_ignore_ascii_case("similar"))
22284 } else {
22285 false
22286 };
22287 if negated {
22288 self.advance();
22289 }
22290 if matches!(self.peek(), Token::Between) {
22291 expr = self.parse_between_tail(expr, negated)?;
22292 {
22293 return Ok(Some(expr));
22294 }
22295 }
22296 if matches!(self.peek(), Token::In) {
22297 if self.suppress_in_tail && !negated {
22298 // POSITION(sub IN str) — IN belongs to the
22299 // enclosing function syntax; stop here.
22300 {
22301 return Ok(None);
22302 }
22303 }
22304 expr = self.parse_in_tail(expr, negated)?;
22305 {
22306 return Ok(Some(expr));
22307 }
22308 }
22309 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22310 // lowers onto the internal __similar_to(expr, pat[, esc]) call
22311 // (the SQL→regex transform runs inside, in the backtracking-
22312 // friendly shape SPG's matcher needs).
22313 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22314 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22315 {
22316 self.advance(); // SIMILAR
22317 self.advance(); // TO
22318 let pattern = self.parse_expr(6)?;
22319 let mut args = alloc::vec![expr, pattern];
22320 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22321 self.advance();
22322 args.push(self.parse_expr(6)?);
22323 }
22324 let call = Expr::FunctionCall {
22325 name: "__similar_to".to_string(),
22326 args,
22327 };
22328 expr = maybe_not(call, negated);
22329 {
22330 return Ok(Some(expr));
22331 }
22332 }
22333 if matches!(self.peek(), Token::Like) {
22334 self.advance();
22335 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22336 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22337 expr = q;
22338 {
22339 return Ok(Some(expr));
22340 }
22341 }
22342 // Pattern at the same precedence as other comparison RHSes —
22343 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22344 let mut pattern = self.parse_expr(6)?;
22345 // `ESCAPE 'c'` — rewrite a literal pattern to the
22346 // default backslash escape at parse time. Custom
22347 // escapes on non-literal patterns would need
22348 // matcher support; error honestly.
22349 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22350 self.advance();
22351 let esc = self.parse_expr(6)?;
22352 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22353 }
22354 expr = Expr::Like {
22355 expr: Box::new(expr),
22356 pattern: Box::new(pattern),
22357 negated,
22358 case_insensitive: false,
22359 };
22360 {
22361 return Ok(Some(expr));
22362 }
22363 }
22364 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22365 // keyword reaches us as a plain identifier.
22366 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22367 self.advance();
22368 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22369 expr = q;
22370 {
22371 return Ok(Some(expr));
22372 }
22373 }
22374 let pattern = self.parse_expr(6)?;
22375 expr = Expr::Like {
22376 expr: Box::new(expr),
22377 pattern: Box::new(pattern),
22378 negated,
22379 case_insensitive: true,
22380 };
22381 {
22382 return Ok(Some(expr));
22383 }
22384 }
22385 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22386 // operator (RLIKE is the alias). It is a keyword, not `~`, and
22387 // matches case-insensitively under the default collation, so it
22388 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22389 // `~*` operator uses, wrapped in NOT when negated.
22390 if self.mysql_dialect
22391 && matches!(self.peek(), Token::Ident(s)
22392 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22393 {
22394 self.advance();
22395 let pattern = self.parse_expr(6)?;
22396 let call = Expr::FunctionCall {
22397 name: String::from("regexp_like"),
22398 args: alloc::vec![
22399 expr,
22400 pattern,
22401 Expr::Literal(Literal::String(String::from("i"))),
22402 ],
22403 };
22404 return Ok(Some(maybe_not(call, negated)));
22405 }
22406 }
22407 let _ = expr;
22408 Ok(None)
22409 }
22410
22411 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22412 let mut lhs = self.parse_unary()?;
22413 let mut chain_len = 0usize;
22414 loop {
22415 // OPERATOR([schema.]op) reduces to its underlying
22416 // operator token before the normal dispatch.
22417 let explicit = self.peek_explicit_operator();
22418 let dispatch = match &explicit {
22419 Some((_, tok)) => self.binop_here(tok),
22420 None => self.binop_here(self.peek()),
22421 };
22422 let Some((op, prec)) = dispatch else {
22423 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22424 // of the symbol family. `binop_here` answers None for them
22425 // because they lower onto function calls rather than a
22426 // BinOp, and the fallback below reads `self.peek()` — the
22427 // word OPERATOR, not the operator. `pg_dump` writes every
22428 // catalog predicate this way, so its first query failed
22429 // and no dump ran:
22430 //
22431 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22432 //
22433 // Collapsing the wrapper to the operator it names puts the
22434 // token where the fallback already looks.
22435 if let Some((next, op_tok)) = explicit {
22436 self.tokens.splice(self.pos..next, [op_tok]);
22437 }
22438 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22439 lhs = e;
22440 chain_len += 1;
22441 if chain_len > MAX_BINARY_CHAIN {
22442 return Err(self.err(alloc::format!(
22443 "more than {MAX_BINARY_CHAIN} chained binary operators"
22444 )));
22445 }
22446 continue;
22447 }
22448 break;
22449 };
22450 if prec < min_prec {
22451 break;
22452 }
22453 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22454 // iteratively but evaluates and drops recursively;
22455 // depth beyond the budget overflows worker stacks.
22456 chain_len += 1;
22457 if chain_len > MAX_BINARY_CHAIN {
22458 return Err(self.err(alloc::format!(
22459 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22460 )));
22461 }
22462 match explicit {
22463 Some((end_pos, _)) => self.pos = end_pos,
22464 None => {
22465 self.advance();
22466 }
22467 }
22468 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22469 // ANY is a bare ident; ALL is a reserved Token. Both
22470 // require an immediate `(` to disambiguate from
22471 // identifier columns named `any` / `all`.
22472 let any_kind = match self.peek() {
22473 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22474 Some(false)
22475 }
22476 Token::Ident(s) | Token::QuotedIdent(s)
22477 if (s.eq_ignore_ascii_case("any")
22478 || s.eq_ignore_ascii_case("some")
22479 || s.eq_ignore_ascii_case("all"))
22480 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22481 {
22482 Some(!s.eq_ignore_ascii_case("all"))
22483 }
22484 _ => None,
22485 };
22486 if let Some(is_any) = any_kind {
22487 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22488 continue;
22489 }
22490 let rhs = self.parse_expr(prec + 1)?;
22491 lhs = Expr::Binary {
22492 lhs: Box::new(lhs),
22493 op,
22494 rhs: Box::new(rhs),
22495 };
22496 }
22497 Ok(lhs)
22498 }
22499
22500 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22501 /// and the array form.
22502 ///
22503 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22504 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22505 /// this block's `Expr` temporaries and four `format!` sites slots in
22506 /// that frame on every level of `((((1))))`, which never reaches it.
22507 #[inline(never)]
22508 fn parse_any_all_rhs(
22509 &mut self,
22510 lhs: Expr,
22511 op: BinOp,
22512 is_any: bool,
22513 ) -> Result<Expr, ParseError> {
22514 self.advance(); // ident
22515 self.advance(); // (
22516 // `x op ANY (SELECT …)` — the quantified-subquery
22517 // form. `= ANY` is exactly IN; the other operators
22518 // lower onto EXISTS over the subquery as a derived
22519 // table, comparing against its single projection
22520 // aliased __v (x's columns resolve correlated).
22521 // ALL is the negated-EXISTS complement; a NULL
22522 // element makes PG return NULL where this lowering
22523 // returns true — the NOT NULL column case (the
22524 // practical one) is exact.
22525 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22526 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22527 // legal PG too (round-151 sibling). Out-of-line
22528 // (#[inline(never)] helper) — this sits on
22529 // parse_expr's recursive frame and the two-armed
22530 // SELECT temporary blew the nesting-budget stack.
22531 let mut sub = self.parse_any_all_select_body()?;
22532 if !matches!(self.peek(), Token::RParen) {
22533 return Err(self.err(alloc::format!(
22534 "expected ')' after ANY/ALL subquery, got {:?}",
22535 self.peek()
22536 )));
22537 }
22538 self.advance();
22539 if sub.items.len() != 1 {
22540 return Err(self.err(alloc::format!(
22541 "ANY/ALL subquery must return one column, got {}",
22542 sub.items.len()
22543 )));
22544 }
22545 if is_any && matches!(op, BinOp::Eq) {
22546 return Ok(Expr::InSubquery {
22547 expr: Box::new(lhs),
22548 subquery: Box::new(sub),
22549 negated: false,
22550 });
22551 }
22552 // The engine's subquery resolvers materialise
22553 // the single-column result into an ARRAY the
22554 // existing AnyAll three-valued eval consumes.
22555 return Ok(Expr::AnyAll {
22556 expr: Box::new(lhs),
22557 op,
22558 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22559 is_any,
22560 });
22561 }
22562 let arr = self.parse_expr(0)?;
22563 if !matches!(self.peek(), Token::RParen) {
22564 return Err(self.err(alloc::format!(
22565 "expected ')' after ANY/ALL argument, got {:?}",
22566 self.peek()
22567 )));
22568 }
22569 self.advance();
22570 Ok(Expr::AnyAll {
22571 expr: Box::new(lhs),
22572 op,
22573 array: Box::new(arr),
22574 is_any,
22575 })
22576 }
22577
22578 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22579 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22580 #[inline(never)]
22581 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22582 self.advance();
22583 let e = self.parse_expr(9)?;
22584 Ok(build_center_call(e))
22585 }
22586
22587 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22588 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22589 /// unary minus.
22590 ///
22591 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22592 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22593 /// the Expr-sized local stays out of that frame.
22594 #[inline(never)]
22595 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22596 self.advance();
22597 let e = self.parse_expr(9)?;
22598 Ok(Expr::FunctionCall {
22599 name: alloc::string::String::from(name),
22600 args: alloc::vec![e],
22601 })
22602 }
22603
22604 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22605 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22606 #[inline(never)]
22607 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22608 self.advance();
22609 let e = self.parse_expr(9)?;
22610 Ok(Expr::FunctionCall {
22611 name: alloc::string::String::from(if vertical {
22612 "isvertical"
22613 } else {
22614 "ishorizontal"
22615 }),
22616 args: alloc::vec![e],
22617 })
22618 }
22619
22620 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22621 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22622 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22623 #[inline(never)]
22624 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22625 self.advance();
22626 let e = self.parse_expr(9)?;
22627 Ok(Expr::Cast {
22628 expr: Box::new(e),
22629 target: CastTarget::Named("binary".to_string()),
22630 })
22631 }
22632
22633 /// The prefix operators that share one shape: take the token, parse
22634 /// an operand at `prec`, wrap it.
22635 ///
22636 /// `#[inline(never)]`, and one function instead of five arms, for the
22637 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22638 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22639 /// debug build gives EVERY arm's locals a slot in the frame, whichever
22640 /// arm runs. `((((1))))` reaches none of these arms and was carrying
22641 /// five `Expr`-sized locals per level for them anyway.
22642 #[inline(never)]
22643 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22644 self.advance();
22645 let e = self.parse_expr(prec)?;
22646 Ok(Expr::Unary {
22647 op,
22648 expr: Box::new(e),
22649 })
22650 }
22651
22652 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22653 /// and separate from it because of the literal folding below and the
22654 /// `format!` temporaries that folding needs.
22655 #[inline(never)]
22656 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22657 self.advance();
22658 // v7.39 (round 549) — fold the sign into an integer literal that
22659 // only fits once it is negative.
22660 //
22661 // `9223372036854775808` is one past i64::MAX, so the lexer hands
22662 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22663 // folds the sign first, so `-9223372036854775808` is a bigint
22664 // there — and `-9223372036854775808 - 1` raises "bigint out of
22665 // range" where SPG quietly answered -9223372036854775809, a value
22666 // no bigint can hold. The arithmetic itself was already checked;
22667 // only the literal's type was wrong.
22668 if let Token::Numeric(lit) = self.peek()
22669 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22670 {
22671 self.advance();
22672 return Ok(Expr::Literal(Literal::Integer(folded)));
22673 }
22674 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22675 // `<->` slotted into 5 and arithmetic shifted up).
22676 let e = self.parse_expr(9)?;
22677 Ok(Expr::Unary {
22678 op: UnOp::Neg,
22679 expr: Box::new(e),
22680 })
22681 }
22682
22683 /// tsquery `!!` prefix negation, lowered to the catalog function.
22684 /// Binds like unary minus. Out-of-line for the frame reason on
22685 /// `parse_unary_op`.
22686 #[inline(never)]
22687 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22688 self.advance();
22689 let e = self.parse_expr(9)?;
22690 Ok(Expr::FunctionCall {
22691 name: String::from("tsquery_not"),
22692 args: alloc::vec![e],
22693 })
22694 }
22695
22696 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22697 match self.peek() {
22698 // NOT binds tighter than AND / XOR / OR but looser than
22699 // comparisons — its operand takes everything ≥ the comparison
22700 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22701 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22702 // was rung 3, behaviour-identical when 3 was unused; AND now
22703 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22704 Token::Not => self.parse_unary_op(UnOp::Not, 4),
22705 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22706 // The body is out-of-line: `parse_unary` is one of the three
22707 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22708 // inline arm here overflowed the native stack in
22709 // `nesting_budget_errors_cleanly` — the guard test caught it,
22710 // exactly as the eval-side cliff did in rounds 346 and 351.
22711 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22712 self.parse_binary_prefix()
22713 }
22714 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22715 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22716 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22717 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22718 Token::Minus => self.parse_prefix_minus(),
22719 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22720 // worked only because the lexer reads it as one signed literal;
22721 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22722 // PG18 and MariaDB take all of them. Binds like unary minus.
22723 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22724 // Bitwise NOT binds like unary minus.
22725 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22726 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22727 // "center of" operator; desugars to center(x). The whole arm
22728 // is out-of-line: parse_unary sits on the per-nesting-level
22729 // frame chain that MAX_NEST_DEPTH is tuned against, so no
22730 // Expr-sized local may live in this frame.
22731 Token::TsMatch => self.parse_prefix_center(),
22732 // v7.39 (round 508) — the prefix operators that are named
22733 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22734 // is length. Out-of-line for the same nesting-frame reason as
22735 // parse_prefix_center — parse_unary sits on the recursive cycle
22736 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22737 // live in this frame.
22738 Token::At => self.parse_prefix_call("abs"),
22739 Token::Hash => self.parse_prefix_call("npoints"),
22740 Token::AtMinusAt => self.parse_prefix_call("length"),
22741 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22742 // "is horizontal" (lseg / line); desugars to the existing
22743 // isvertical()/ishorizontal() functions. Out-of-line for the
22744 // same nesting-frame reason as parse_prefix_center.
22745 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22746 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22747 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22748 _ => self.parse_atom(),
22749 }
22750 }
22751
22752 /// Parse a parenthesised scalar subquery body after the caller has consumed
22753 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22754 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22755 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22756 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22757 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22758 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22759 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22760 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22761 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22762 /// tips the deep-nesting test into a stack overflow).
22763 #[inline(never)]
22764 fn array_subquery_ahead(&self) -> bool {
22765 if !matches!(self.peek(), Token::LParen) {
22766 return false;
22767 }
22768 matches!(
22769 self.tokens.get(self.pos + 1),
22770 Some(Token::Select | Token::Values)
22771 ) || matches!(
22772 self.tokens.get(self.pos + 1),
22773 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22774 )
22775 }
22776
22777 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22778 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22779 /// locals stay off parse_atom's recursive frame (round 105).
22780 #[inline(never)]
22781 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22782 self.advance(); // consume `[`
22783 let mut items: Vec<Expr> = Vec::new();
22784 if !matches!(self.peek(), Token::RBracket) {
22785 loop {
22786 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22787 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22788 if matches!(self.peek(), Token::LBracket) {
22789 items.push(self.parse_array_bracket_body()?);
22790 } else {
22791 items.push(self.parse_expr(0)?);
22792 }
22793 match self.peek() {
22794 Token::Comma => {
22795 self.advance();
22796 }
22797 Token::RBracket => break,
22798 other => {
22799 return Err(self.err(alloc::format!(
22800 "expected ',' or ']' in ARRAY literal, got {other:?}"
22801 )));
22802 }
22803 }
22804 }
22805 }
22806 self.advance(); // consume `]`
22807 Ok(Expr::Array(items))
22808 }
22809
22810 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22811 /// is already consumed; the current token is `(`. Desugars to a scalar
22812 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22813 /// the subquery's single-column rows in order — reusing the existing
22814 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22815 /// keeps the large `Statement` local off parse_atom's recursive frame.
22816 #[inline(never)]
22817 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22818 self.advance(); // consume `(`
22819 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22820 if w.eq_ignore_ascii_case("with"));
22821 let sub = if is_with {
22822 self.advance(); // WITH
22823 self.parse_with_cte_then_select()?
22824 } else {
22825 self.parse_select_stmt()?
22826 };
22827 if !matches!(self.peek(), Token::RParen) {
22828 return Err(self.err(alloc::format!(
22829 "expected ')' to close ARRAY(subquery), got {:?}",
22830 self.peek()
22831 )));
22832 }
22833 self.advance(); // consume `)`
22834 // Reuse the parser to build the array_agg wrapper from the subquery's
22835 // canonical text — avoids hand-constructing the derived-table AST.
22836 let wrapper = alloc::format!(
22837 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22838 );
22839 let stmt = parse_statement(&wrapper)
22840 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22841 let Statement::Select(sel) = stmt else {
22842 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22843 };
22844 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22845 }
22846
22847 #[inline(never)]
22848 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22849 let inner = if is_with {
22850 self.advance(); // WITH
22851 self.parse_with_cte_then_select()?
22852 } else {
22853 self.parse_select_stmt()?
22854 };
22855 match self.advance() {
22856 Token::RParen => {
22857 let Statement::Select(s) = inner else {
22858 return Err(ParseError {
22859 message: "scalar subquery body must be a SELECT".into(),
22860 token_pos: self.consumed_pos(),
22861 });
22862 };
22863 Ok(Expr::ScalarSubquery(Box::new(s)))
22864 }
22865 other => Err(ParseError {
22866 message: format!("expected ')' after scalar subquery, got {other:?}"),
22867 token_pos: self.consumed_pos(),
22868 }),
22869 }
22870 }
22871
22872 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22873 /// literals. The lexer splits them into an ident + string; recombine
22874 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22875 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22876 /// frame for the `body` / `bits` strings and their char loops (the
22877 /// round-367 frame cliff, M20).
22878 #[inline(never)]
22879 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22880 let is_hex = match self.peek() {
22881 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22882 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22883 _ => return None,
22884 };
22885 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22886 return None;
22887 }
22888 // v7.39.3 — where the LITERAL starts, because the errors below
22889 // are about the literal and both engines point at it. `err`
22890 // reports the CURRENT token, which by then is the one after the
22891 // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22892 // `near '…'` snippet — which runs from the reported position to
22893 // the end — came out empty where MySQL 9.7.2 says `near
22894 // 'x'123''`.
22895 let lit_pos = self.pos;
22896 self.advance();
22897 let Token::String(body) = self.advance() else {
22898 unreachable!("guarded above");
22899 };
22900 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22901 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22902 // (hex pairs, even count required — MariaDB errors on an odd
22903 // count); `b'1010'` packs its bits big-endian, left-padded to a
22904 // byte. Lower both onto the bytea cast.
22905 if self.mysql_dialect {
22906 if is_hex {
22907 if body.len() % 2 == 1 {
22908 return Some(Err(self.err_at(
22909 lit_pos,
22910 alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22911 )));
22912 }
22913 for c in body.chars() {
22914 if !c.is_ascii_hexdigit() {
22915 return Some(Err(self.err_at(
22916 lit_pos,
22917 alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22918 )));
22919 }
22920 }
22921 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22922 }
22923 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22924 return Some(Err(self.err_at(
22925 lit_pos,
22926 alloc::format!("invalid binary digit {bad:?} in b'…'"),
22927 )));
22928 }
22929 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22930 }
22931 let bits = if is_hex {
22932 let mut out = String::with_capacity(body.len() * 4);
22933 for c in body.chars() {
22934 let Some(d) = c.to_digit(16) else {
22935 // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22936 // own quoting: `"g" is not a valid hexadecimal
22937 // digit` (measured, with the caret on the literal).
22938 return Some(Err(self.err_at(
22939 lit_pos,
22940 alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22941 )));
22942 };
22943 out.push_str(&alloc::format!("{d:04b}"));
22944 }
22945 out
22946 } else {
22947 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22948 return Some(Err(self.err_at(
22949 lit_pos,
22950 alloc::format!("\"{bad}\" is not a valid binary digit"),
22951 )));
22952 }
22953 body
22954 };
22955 // Route through the postfix-cast loop so a chained cast like
22956 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22957 // of erroring at the `::`.
22958 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22959 // literal keeps its exact length, while an explicit `::bit` cast is
22960 // bit(1) with pad/truncate semantics (PG).
22961 Some(self.finish_postfix_casts(Expr::Cast {
22962 expr: Box::new(Expr::Literal(Literal::String(bits))),
22963 target: CastTarget::Named("__bit_literal".to_string()),
22964 }))
22965 }
22966
22967 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22968 if let Some(res) = self.try_parse_bit_string_literal() {
22969 return res;
22970 }
22971 // v7.40.11 — `<alias>.*` in an EXPRESSION is the whole row, the
22972 // same thing the bare alias is.
22973 //
22974 // Reported against 7.40.9: `to_jsonb(t.*)`, `row_to_json(t.*)`,
22975 // `pg_column_size(t.*)` and `count(t.*)` were all
22976 // `syntax error at or near "*"`, while `to_jsonb(t)` — the same
22977 // value, differently spelled — worked. Measured on PG 18.6, the
22978 // two spellings answer byte-identically:
22979 //
22980 // to_jsonb(t.*) {"a": 1, "b": "x"}
22981 // to_jsonb(t) {"a": 1, "b": "x"}
22982 //
22983 // Here rather than in the argument list: nothing else in an
22984 // expression position is `ident . *`, and a select item's own
22985 // `t.*` is recognised before `parse_expr` is ever called, so
22986 // `SELECT t.* FROM t` keeps expanding to the column list.
22987 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone()
22988 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22989 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
22990 {
22991 self.advance();
22992 self.advance();
22993 self.advance();
22994 return Ok(Expr::Column(ColumnName {
22995 qualifier: None,
22996 name: q,
22997 }));
22998 }
22999 let tok_pos = self.pos;
23000 match self.advance() {
23001 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
23002 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
23003 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
23004 // carrying the source mantissa + scale so no precision is lost. A
23005 // literal too wide for i128 falls back to double precision.
23006 // Out-of-line (#[inline(never)]) — this arm sits on the
23007 // parse_expr recursion chain; its expansion locals must not
23008 // widen the recursive frame (debug frame-cliff discipline).
23009 Token::Numeric(s) => match numeric_token_to_literal(s) {
23010 Ok(lit) => Ok(Expr::Literal(lit)),
23011 Err(msg) => Err(self.err(msg)),
23012 },
23013 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
23014 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
23015 // (the lexer only emits this token in the MySQL dialect). Lower
23016 // onto the existing bytea cast; out-of-line to keep this arm off
23017 // the parse recursion frame.
23018 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
23019 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
23020 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
23021 Token::Null => Ok(Expr::Literal(Literal::Null)),
23022 // v6.1.1 — `$N` placeholder. The actual Value lookup
23023 // happens in the engine eval path against the prepared-
23024 // statement bind buffer.
23025 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
23026 Token::LParen => {
23027 // v4.10: `(SELECT ...)` in expression position is a
23028 // scalar subquery; otherwise it's a parenthesised
23029 // expression. Peek for SELECT keyword to dispatch.
23030 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
23031 // lexes as Ident("with") (not a reserved token). The subquery body
23032 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
23033 // so its large `Statement` local stays out of parse_atom's stack
23034 // frame — parse_atom is on the recursive `((…))` cycle and the
23035 // nesting budget is tuned to its frame size).
23036 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23037 if s.eq_ignore_ascii_case("with"));
23038 if matches!(self.peek(), Token::Select) || is_with {
23039 self.parse_paren_scalar_subquery(is_with)
23040 } else {
23041 let e = self.parse_expr(0)?;
23042 // `(a, b, …)` — a row constructor. Valid only
23043 // in front of a comparison operator or [NOT]
23044 // IN; both expand at parse time (lexicographic
23045 // comparison / OR'd row equalities).
23046 if matches!(self.peek(), Token::Comma) {
23047 let mut row = alloc::vec![e];
23048 while matches!(self.peek(), Token::Comma) {
23049 self.advance();
23050 row.push(self.parse_expr(0)?);
23051 }
23052 if !matches!(self.peek(), Token::RParen) {
23053 return Err(self.err(alloc::format!(
23054 "expected ')' after row constructor, got {:?}",
23055 self.peek()
23056 )));
23057 }
23058 self.advance();
23059 // A bare `(a, b, …)` row constructor can carry postfix
23060 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
23061 // early return here skips parse_atom's tail postfix
23062 // pass, so fold casts in explicitly. For the
23063 // comparison / predicate forms nothing postfix follows,
23064 // so this is a no-op.
23065 return self
23066 .parse_row_comparison_tail(row)
23067 .and_then(|e| self.finish_postfix_casts(e));
23068 }
23069 match self.advance() {
23070 Token::RParen => Ok(e),
23071 other => Err(ParseError {
23072 message: format!("expected ')', got {other:?}"),
23073 token_pos: self.consumed_pos(),
23074 }),
23075 }
23076 }
23077 }
23078 Token::LBracket => self.parse_vector_literal_body(),
23079 Token::Extract => self.parse_extract_atom(),
23080 Token::Interval => self.parse_interval_atom(),
23081 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
23082 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
23083 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
23084 // expression position calling the PG `left(string, n)` /
23085 // `right(string, n)` function; rebuild the AST as a regular
23086 // function call so the engine's apply_function dispatch picks
23087 // it up. Delegated to a #[inline(never)] helper so its locals
23088 // don't bloat this recursive `parse_atom` frame (the nesting
23089 // budget in `enter_nested` is tuned to parse_atom's size).
23090 Token::Left if matches!(self.peek(), Token::LParen) => {
23091 self.parse_lr_string_function_call("left")
23092 }
23093 Token::Right if matches!(self.peek(), Token::LParen) => {
23094 self.parse_lr_string_function_call("right")
23095 }
23096 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
23097 // token; we match on the bare ident. NOT is a token
23098 // (consumed in the comparison rung), but `EXISTS (...)`
23099 // at the top of an expression starts here.
23100 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
23101 self.parse_exists_atom(false)
23102 }
23103 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
23104 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
23105 // CASE is a bare ident; we dispatch on lowercase match.
23106 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
23107 self.parse_case_atom()
23108 }
23109 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
23110 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
23111 // '…'`. Lower onto the ::cast node so the existing
23112 // runtime text→date/timestamp paths do the parsing. The
23113 // string must follow immediately, else the ident stays a
23114 // plain column reference.
23115 Token::Ident(s)
23116 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
23117 && matches!(self.peek(), Token::String(_)) =>
23118 {
23119 let target =
23120 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
23121 let Token::String(lit) = self.advance() else {
23122 unreachable!("peek guaranteed a string token");
23123 };
23124 Ok(Expr::Cast {
23125 expr: Box::new(Expr::Literal(Literal::String(lit))),
23126 target,
23127 })
23128 }
23129 // v7.39 (round 221) — the SQL-standard long spellings:
23130 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
23131 // TIME ZONE '…'`. Consume the modifier and lower to the same
23132 // typed-literal cast (`timetz` / `timestamptz` for WITH).
23133 Token::Ident(s)
23134 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
23135 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
23136 || w.eq_ignore_ascii_case("without"))
23137 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
23138 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
23139 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
23140 {
23141 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
23142 self.advance(); // WITH / WITHOUT
23143 self.advance(); // TIME
23144 self.advance(); // ZONE
23145 let Token::String(lit) = self.advance() else {
23146 unreachable!("guard checked a string token");
23147 };
23148 let base = s.to_ascii_lowercase();
23149 let target = match (base.as_str(), with_tz) {
23150 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
23151 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
23152 (_, true) => CastTarget::Timestamptz,
23153 (_, false) => CastTarget::Timestamp,
23154 };
23155 Ok(Expr::Cast {
23156 expr: Box::new(Expr::Literal(Literal::String(lit))),
23157 target,
23158 })
23159 }
23160 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23161 // gathers the subquery's single-column rows (in its row order)
23162 // into an array. Desugared to `array_agg` over the subquery as a
23163 // derived table; out-of-line to keep parse_atom's frame small (it
23164 // sits on the recursive nesting-budget cycle).
23165 Token::Ident(s) | Token::QuotedIdent(s)
23166 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23167 {
23168 self.parse_array_subquery()
23169 }
23170 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23171 // is not a reserved token; we match by case-insensitive
23172 // ident. The opening `[` must follow immediately. v7.39 (read01
23173 // round 105) — the body moved out-of-line so its `Vec`/loop locals
23174 // leave parse_atom's frame (which sits on the nesting-budget cycle).
23175 Token::Ident(s) | Token::QuotedIdent(s)
23176 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23177 {
23178 self.parse_array_literal_body()
23179 }
23180 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23181 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23182 // We special-case before the generic ident dispatch so
23183 // the AGAINST clause never reaches the function-call
23184 // loop (which would mis-read `(cols) AGAINST` as a
23185 // call with no trailing modifier). The shape is
23186 // rewritten to a Boolean OR over per-column
23187 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23188 // term)` so the existing FTS evaluator handles
23189 // semantics — the fulltext-GIN built at CREATE TABLE
23190 // time is currently a "real index that survives dump
23191 // round-trip"; the planner hook that actually uses
23192 // it for posting-list intersection lands in a later
23193 // sub-phase (Phase 2.2b) without touching this surface.
23194 Token::Ident(s) | Token::QuotedIdent(s)
23195 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23196 {
23197 self.parse_match_against_atom()
23198 }
23199 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23200 // v7.37.43-T4 — PG-unreserved keywords are legal column /
23201 // alias names in expression context too. `release` appears
23202 // in sentori `0003_partition_events.sql` as both a column
23203 // reference (SELECT … release …) and an INSERT column list
23204 // entry. Mirrors `expect_ident_like`'s expansion of the
23205 // identifier set.
23206 other if unreserved_keyword_text(&other).is_some() => {
23207 let s = unreserved_keyword_text(&other).unwrap();
23208 self.finish_ident_atom(s)
23209 }
23210 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23211 // only inside `SET` before, so `SELECT @@autocommit` — which
23212 // every MySQL connector asks at handshake — was a parse error.
23213 // MariaDB accepts the bare, `@@session.` and `@@global.`
23214 // spellings alike and answers from the session's own value.
23215 Token::SessionVar(v) => {
23216 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23217 // has nothing to do with a `@@` engine setting: its own
23218 // per-session namespace, and an unset one reads NULL instead
23219 // of raising. Stripping every `@` (as this did) made `@x` and
23220 // `@@x` the same node, so `SELECT @x` answered "Unknown
23221 // system variable".
23222 Ok(variable_ref_atom(&v))
23223 }
23224 other => Err(ParseError {
23225 message: format!("unexpected token {other:?} in expression"),
23226 token_pos: tok_pos,
23227 }),
23228 }
23229 // After parsing the atom, fold any postfix `::vector` casts.
23230 .and_then(|atom| self.finish_postfix_casts(atom))
23231 }
23232
23233 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23234 /// Both bind tighter than any binary op.
23235 /// Shared cast-target parser for postfix `::TYPE` and the
23236 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23237 /// If the next tokens are `( N )`, consume them and return the canonical
23238 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23239 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23240 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23241 if !matches!(self.peek(), Token::LParen) {
23242 return None;
23243 }
23244 self.advance(); // (
23245 let n = match self.advance() {
23246 Token::Integer(n) => n,
23247 _ => return Some(base.to_string()), // malformed → drop precision
23248 };
23249 if matches!(self.peek(), Token::RParen) {
23250 self.advance();
23251 }
23252 Some(alloc::format!("{base}({n})"))
23253 }
23254
23255 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23256 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23257 // schema-qualifies every cast target, and `pg_catalog.X` names
23258 // exactly the builtin type X. Consume the qualifier and let
23259 // the ordinary target parse decide.
23260 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23261 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23262 {
23263 self.advance();
23264 self.advance();
23265 }
23266 let target = match self.advance() {
23267 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23268 "int" | "integer" | "int4" => {
23269 if matches!(self.peek(), Token::LBracket)
23270 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23271 {
23272 self.advance();
23273 self.advance();
23274 CastTarget::IntArray
23275 } else {
23276 CastTarget::Int
23277 }
23278 }
23279 "bigint" | "int8" => {
23280 if matches!(self.peek(), Token::LBracket)
23281 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23282 {
23283 self.advance();
23284 self.advance();
23285 CastTarget::BigIntArray
23286 } else {
23287 CastTarget::BigInt
23288 }
23289 }
23290 "float" | "double" => CastTarget::Float,
23291 "text" => {
23292 // v7.10.11 — `::TEXT[]` widens to TextArray.
23293 if matches!(self.peek(), Token::LBracket)
23294 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23295 {
23296 self.advance();
23297 self.advance();
23298 CastTarget::TextArray
23299 } else {
23300 CastTarget::Text
23301 }
23302 }
23303 "bool" | "boolean" => CastTarget::Bool,
23304 "vector" => CastTarget::Vector,
23305 "date" => CastTarget::Date,
23306 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23307 // seconds precision through the Named path (the engine rounds
23308 // the sub-second field); bare `::timestamp` keeps the fast arm.
23309 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23310 Some(named) => CastTarget::Named(named),
23311 None => CastTarget::Timestamp,
23312 },
23313 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23314 Some(named) => CastTarget::Named(named),
23315 None => CastTarget::Timestamptz,
23316 },
23317 "interval" => CastTarget::Interval,
23318 "json" => CastTarget::Json,
23319 "jsonb" => CastTarget::Jsonb,
23320 // v7.39 (round 694) — these have dedicated CastTarget
23321 // variants, so they never reached the postfix `[]` handling
23322 // further down and `::regtype[]` was a SYNTAX error at the
23323 // `]`. PG has an array type for every scalar; take the
23324 // suffix here and hand the canonical `<ty>_array` name to
23325 // the engine, the same shape every other array cast uses.
23326 "regtype" if self.peek_postfix_array_brackets() => {
23327 self.advance();
23328 self.advance();
23329 CastTarget::Named(alloc::string::String::from("regtype_array"))
23330 }
23331 "regclass" if self.peek_postfix_array_brackets() => {
23332 self.advance();
23333 self.advance();
23334 CastTarget::Named(alloc::string::String::from("regclass_array"))
23335 }
23336 "regtype" => CastTarget::RegType,
23337 "regclass" => CastTarget::RegClass,
23338 // v7.12.0 — `::tsvector` / `::tsquery`.
23339 // Engine decodes the LHS text via the PG
23340 // external form parser.
23341 // v7.39 (round 352, M8) — MySQL's own cast targets.
23342 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23343 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23344 // such type, so they are taken only in that dialect and
23345 // fall through to the "type does not exist" arm otherwise.
23346 "signed" | "unsigned" if self.mysql_dialect => {
23347 if matches!(self.peek(), Token::Ident(k)
23348 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23349 {
23350 self.advance();
23351 }
23352 CastTarget::Named(s.to_ascii_lowercase())
23353 }
23354 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23355 // in MySQL: MariaDB answers '123' where the SQL-standard
23356 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23357 // Truncating a number to its first digit is a wrong answer
23358 // with no error, so the MySQL session gets MySQL's reading.
23359 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23360 CastTarget::Text
23361 }
23362 "tsvector" => CastTarget::TsVector,
23363 "tsquery" => CastTarget::TsQuery,
23364 // v7.17.0 — `::uuid`. Engine decodes the LHS
23365 // text via `spg_storage::parse_uuid_str`.
23366 "uuid" => CastTarget::Uuid,
23367 // v7.18 — `::bytea`. Engine decodes the LHS
23368 // text via the PG hex form (`'\xdeadbeef'`)
23369 // or escape form (`'\\x05\\x00'`). Closes
23370 // mailrs D-pre #3 reverse-acceptance gap.
23371 "bytea" => CastTarget::Bytea,
23372 // v7.37.5 ship triage — generic typed-cast escape.
23373 // Anything the long-tail PG type ident table knows
23374 // about(network/bit/geometry/multirange/etc.)flows
23375 // through `CastTarget::Named(canonical)`; the engine
23376 // resolves via `column_type_to_data_type` and dispatches
23377 // through the typed `coerce_value` path. Truly
23378 // unrecognised idents still hit the error arm below
23379 // because the engine rejects them.
23380 other => {
23381 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23382 // `::varchar(255)`, etc. Capture into the canonical
23383 // `name(p,s)` form so `type_name_to_data_type` can
23384 // reconstruct the `DataType::Numeric { precision,
23385 // scale }` (and similar param-carrying types).
23386 let mut name = other.to_string();
23387 // v7.39 (round 281) — `::bit varying(3)` is two
23388 // words; fold the tail in so the typmod reaches the
23389 // type resolver instead of tripping the parser.
23390 if name.eq_ignore_ascii_case("bit")
23391 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23392 {
23393 self.advance();
23394 name = alloc::string::String::from("varbit");
23395 }
23396 // v7.39 (round 613) — `::character varying` is the same
23397 // two-word shape and had no fold, so the `varying` was
23398 // left behind and the cast became a bare `character`,
23399 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23400 // `a` where PG answers `ab`. Silently, and for a spelling
23401 // pg_dump writes.
23402 if name.eq_ignore_ascii_case("character")
23403 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23404 {
23405 self.advance();
23406 name = alloc::string::String::from("varchar");
23407 }
23408 if matches!(self.peek(), Token::LParen) {
23409 let mut buf = alloc::string::String::from("(");
23410 let mut depth = 0usize;
23411 loop {
23412 match self.advance() {
23413 Token::LParen => {
23414 depth += 1;
23415 if depth > 1 {
23416 buf.push('(');
23417 }
23418 }
23419 Token::RParen => {
23420 depth -= 1;
23421 if depth == 0 {
23422 buf.push(')');
23423 break;
23424 }
23425 buf.push(')');
23426 }
23427 Token::Comma => buf.push(','),
23428 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23429 // v7.39 (round 273) — a minus used to fall
23430 // into the catch-all below and vanish, so
23431 // `::numeric(10,-2)` reached the engine as
23432 // the text `numeric(10,2)` and silently
23433 // rounded to two DECIMALS instead of to
23434 // hundreds. A dropped token is not a
23435 // no-op when it carries a sign.
23436 Token::Minus => buf.push('-'),
23437 Token::Eof => break,
23438 _ => {}
23439 }
23440 }
23441 name.push_str(&buf);
23442 }
23443 // Optional postfix `[]` widens to the array form —
23444 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23445 // The engine's `type_name_to_data_type` recognises
23446 // the canonical `<ty>_array` form.
23447 if matches!(self.peek(), Token::LBracket)
23448 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23449 {
23450 self.advance();
23451 self.advance();
23452 name.push_str("_array");
23453 }
23454 CastTarget::Named(name)
23455 }
23456 },
23457 Token::Interval => CastTarget::Interval,
23458 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23459 // "char" (oid 18, SPG Char1 — distinct from bare `char`
23460 // = char(1)); other quoted names resolve like idents.
23461 Token::QuotedIdent(q) => {
23462 if q.eq_ignore_ascii_case("char") {
23463 CastTarget::Named("char1".into())
23464 } else {
23465 CastTarget::Named(q.to_ascii_lowercase())
23466 }
23467 }
23468 other => {
23469 return Err(ParseError {
23470 message: format!("expected type ident after `::`, got {other:?}"),
23471 token_pos: self.consumed_pos(),
23472 });
23473 }
23474 };
23475 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23476 // target to its array sibling. Closed-enum arms (Bool /
23477 // SmallInt / Numeric / Float / Date / …) didn't carry the
23478 // explicit widening that Text / Int / BigInt did, so
23479 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23480 // error. The widening here mirrors the per-arm Text /
23481 // Int / BigInt logic above + folds the new ζ-A first-class
23482 // types through `CastTarget::Named("<ty>_array")`.
23483 if matches!(self.peek(), Token::LBracket)
23484 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23485 {
23486 let widened = match &target {
23487 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23488 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23489 // v7.39 (round 326, V43) — the two temporal types stay
23490 // distinct. Both used to widen to `timestamptz_array`, so
23491 // `::timestamp[]` named the wrong target in its own error
23492 // message and lost the zone-less identity on the way.
23493 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23494 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23495 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23496 CastTarget::Json | CastTarget::Jsonb => {
23497 Some(CastTarget::Named("jsonb_array".to_string()))
23498 }
23499 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23500 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23501 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23502 CastTarget::Named(name) => {
23503 let mut a = name.clone();
23504 a.push_str("_array");
23505 Some(CastTarget::Named(a))
23506 }
23507 // Int / BigInt / Text / Vector / TsVector / TsQuery /
23508 // RegType / RegClass / TextArray / IntArray /
23509 // BigIntArray already finalised — leave as is.
23510 _ => None,
23511 };
23512 if let Some(w) = widened {
23513 self.advance();
23514 self.advance();
23515 return Ok(w);
23516 }
23517 }
23518 Ok(target)
23519 }
23520
23521 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23522 loop {
23523 // v7.38 (read01, T9) — composite field access `(expr).field`.
23524 // A bare `a.b` is consumed as a qualified column inside the ident
23525 // atom, so a Dot only survives to this postfix position when the
23526 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23527 // `.*` whole-row expansion is not handled here (projection-level).
23528 if matches!(self.peek(), Token::Dot)
23529 && matches!(
23530 self.tokens.get(self.pos + 1),
23531 Some(Token::Ident(_) | Token::QuotedIdent(_))
23532 )
23533 {
23534 self.advance(); // .
23535 let field = match self.advance() {
23536 Token::Ident(s) | Token::QuotedIdent(s) => s,
23537 other => {
23538 return Err(
23539 self.err(format!("expected a field name after '.', got {other:?}"))
23540 );
23541 }
23542 };
23543 expr = Expr::FieldAccess {
23544 base: Box::new(expr),
23545 field,
23546 };
23547 continue;
23548 }
23549 if matches!(self.peek(), Token::DoubleColon) {
23550 self.advance();
23551 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23552 // target set to include INTERVAL (reserved Token),
23553 // TIMESTAMPTZ, and PG catalog regtype / regclass.
23554 // mailrs follow-up H3a + H3b.
23555 let target = self.parse_cast_target()?;
23556 expr = Expr::Cast {
23557 expr: Box::new(expr),
23558 target,
23559 };
23560 continue;
23561 }
23562 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23563 // returns NULL for out-of-range. Multiple subscripts
23564 // chain: `a[i][j]` parses left-to-right.
23565 if matches!(self.peek(), Token::LBracket) {
23566 self.advance();
23567 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23568 // bare index stays a subscript.
23569 let lo = if matches!(self.peek(), Token::Colon) {
23570 None
23571 } else {
23572 Some(self.parse_expr(0)?)
23573 };
23574 if matches!(self.peek(), Token::Colon) {
23575 self.advance();
23576 let hi = if matches!(self.peek(), Token::RBracket) {
23577 None
23578 } else {
23579 Some(Box::new(self.parse_expr(0)?))
23580 };
23581 if !matches!(self.peek(), Token::RBracket) {
23582 return Err(self.err(alloc::format!(
23583 "expected ']' after array slice, got {:?}",
23584 self.peek()
23585 )));
23586 }
23587 self.advance();
23588 expr = Expr::ArraySlice {
23589 target: Box::new(expr),
23590 lo: lo.map(Box::new),
23591 hi,
23592 };
23593 continue;
23594 }
23595 let index = lo.expect("non-colon branch parsed an index");
23596 if !matches!(self.peek(), Token::RBracket) {
23597 return Err(self.err(alloc::format!(
23598 "expected ']' after array index, got {:?}",
23599 self.peek()
23600 )));
23601 }
23602 self.advance();
23603 expr = Expr::ArraySubscript {
23604 target: Box::new(expr),
23605 index: Box::new(index),
23606 };
23607 continue;
23608 }
23609 // `expr AT TIME ZONE zone` — lowers to PG's own function
23610 // form timezone(zone, expr); the scalar implements the
23611 // offset shift (named zones error there — no tzdata).
23612 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23613 && matches!(self.tokens.get(self.pos + 1),
23614 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23615 && matches!(self.tokens.get(self.pos + 2),
23616 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23617 {
23618 self.advance(); // AT
23619 self.advance(); // TIME
23620 self.advance(); // ZONE
23621 // Zone at comparison precedence so AND/OR stay out.
23622 let zone = self.parse_expr(6)?;
23623 expr = Expr::FunctionCall {
23624 name: "timezone".to_string(),
23625 args: alloc::vec![zone, expr],
23626 };
23627 continue;
23628 }
23629 // `expr COLLATE "name"` — SPG's single text ordering IS
23630 // byte order, i.e. the C collation. The byte-order
23631 // spellings absorb as no-ops; a locale collation would
23632 // silently sort differently from PG, so it errors
23633 // honestly instead.
23634 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23635 self.advance();
23636 let mut cname = match self.advance() {
23637 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23638 other => {
23639 return Err(self.err(alloc::format!(
23640 "expected collation name after COLLATE, got {other:?}"
23641 )));
23642 }
23643 };
23644 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23645 // is how `pg_dump` writes the default one:
23646 // `… COLLATE pg_catalog.default`. Reading a single token
23647 // left the SCHEMA as the name, so the clause was refused
23648 // as an unsupported locale collation and no dump ran.
23649 if matches!(self.peek(), Token::Dot) {
23650 // v7.39.2 — the qualifier is DROPPED (SPG is single
23651 // schema) but it is checked first. PostgreSQL 18.6
23652 // answers `schema "nosuch_schema" does not exist` for
23653 // one it has never heard of, and dropping it unread
23654 // meant `COLLATE nosuch_schema."C"` succeeded here —
23655 // a name that names nothing, accepted.
23656 let schema = cname.to_ascii_lowercase();
23657 if !matches!(
23658 schema.as_str(),
23659 "pg_catalog" | "public" | "information_schema"
23660 ) {
23661 return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23662 }
23663 self.advance();
23664 cname = match self.advance() {
23665 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23666 // `default` lexes as a KEYWORD, and it is the name
23667 // pg_dump writes — the same trap round 535 hit with
23668 // TABLE / INDEX / FULL.
23669 Token::Default => alloc::string::String::from("default"),
23670 other => {
23671 return Err(self.err(alloc::format!(
23672 "expected collation name after COLLATE, got {other:?}"
23673 )));
23674 }
23675 };
23676 }
23677 let lc = cname.to_ascii_lowercase();
23678 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23679 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23680 // family / `binary`) forces byte-wise, which is exactly
23681 // what `BINARY expr` does — lower onto that so every fold
23682 // site (comparison, LIKE, ORDER BY) suppresses via
23683 // `is_binary_coerced`. A `_ci` family override folds, and
23684 // under the MySQL dialect the default already folds, so it
23685 // absorbs as a no-op; likewise the C / byte-order spellings.
23686 // v7.39.2 — against MySQL's own list, not against the
23687 // shape of the name. `nosuch_bin` took this shortcut and
23688 // became a BINARY cast; `nosuch_ci` took the one below
23689 // and was absorbed as a no-op. Either way the client
23690 // named a collation that does not exist and was told
23691 // nothing. An unknown name now falls through to the
23692 // node, and the engine refuses it.
23693 let real = crate::charset::is_mysql_collation(&lc);
23694 // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23695 //
23696 // They are not the same thing, and folding them together
23697 // lost a bit. Measured on MySQL 9.7.2 with the connection
23698 // on utf8mb4:
23699 //
23700 // ```text
23701 // 'a ' = 'a' COLLATE utf8mb4_bin 1 PAD SPACE
23702 // 'a ' = 'a' COLLATE utf8mb4_0900_bin 0 NO PAD
23703 // 'AB' = 'ab' COLLATE utf8mb4_bin 0 byte-wise
23704 // ```
23705 //
23706 // The BINARY cast carries "byte-wise" and, with it,
23707 // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23708 // the first line. Keeping the node lets `text_compare_of`
23709 // read the NAME and settle the two bits separately: it
23710 // does not fold (`folds_case` says so) and it does pad
23711 // (`pads_space` says so), while `is_byte_wise` still
23712 // keeps the ORDERING off the locale.
23713 if self.mysql_dialect && real && lc == "binary" {
23714 expr = Expr::Cast {
23715 expr: alloc::boxed::Box::new(expr),
23716 target: CastTarget::Named("binary".to_string()),
23717 };
23718 continue;
23719 }
23720 let mysql_ci = self.mysql_dialect
23721 && ((real && lc.ends_with("_ci"))
23722 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23723 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23724 // goes to the lowering channel, the byte-order spellings
23725 // included. Round 691 recorded only the names the old
23726 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23727 // absorbed as a no-op — and once a column could declare a
23728 // collation, absorbing the clause meant the COLUMN's
23729 // collation won where the query had asked for bytes.
23730 if self.in_order_by_key && !mysql_ci {
23731 self.order_key_collation = Some(cname);
23732 continue;
23733 }
23734 // v7.39.2 — the clause becomes a NODE rather than being
23735 // refused or absorbed.
23736 //
23737 // What stood here refused the locale names and SILENTLY
23738 // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23739 // answered `t` where PostgreSQL 18.6 answers `f`: the one
23740 // family it let through is the one where dropping it
23741 // changes the answer. Absorbing is only correct when the
23742 // collation asked for is the one the comparison would use
23743 // anyway, and that depends on the DATABASE — which the
23744 // parser cannot see. So it rides along and the engine,
23745 // which can, decides.
23746 //
23747 // `collate_derive` already modelled `Explicit(name)` and
23748 // had no way to be handed one.
23749 // v7.39.2 — a MySQL spelling does not exist on the
23750 // PostgreSQL wire, and THIS is where the wire is known.
23751 //
23752 // The check lived in the evaluator first and asked
23753 // `ctx.mysql_dialect`, which the INSERT path builds as a
23754 // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23755 // in a MySQL session was refused for a collation that
23756 // does not exist on a wire it was not on. Making that
23757 // context truthful would change INSERT-time evaluation
23758 // in other ways as a side effect; the parser already
23759 // gates the introducer on the same flag and is the
23760 // honest place to ask.
23761 if !self.mysql_dialect
23762 && (lc.ends_with("_ci")
23763 || lc.ends_with("_cs")
23764 || lc.ends_with("_bin")
23765 || lc == "binary"
23766 || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23767 {
23768 return Err(self.err(alloc::format!(
23769 "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23770 )));
23771 }
23772 // v7.39.3 — the node is built for EVERY name, `_ci`
23773 // included.
23774 //
23775 // A MySQL `_ci` spelling used to be absorbed here on the
23776 // reasoning that a MySQL session folds anyway, so the
23777 // clause asked for what it would have got. That stopped
23778 // being true when the fold learned to read the session's
23779 // collation NAME: under `SET NAMES utf8mb4 COLLATE
23780 // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23781 // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23782 // that would have made it 1 had been dropped in the
23783 // parser. Absorbing is only ever correct when the
23784 // collation asked for is the one the comparison would use
23785 // anyway, and the parser cannot know that — the same
23786 // reasoning already written above for the byte-order
23787 // spellings, applied to the family it had exempted.
23788 expr = Expr::Collate {
23789 expr: alloc::boxed::Box::new(expr),
23790 collation: cname,
23791 };
23792 continue;
23793 }
23794 return Ok(expr);
23795 }
23796 }
23797
23798 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23799 /// the first token that is not one. Schema qualifiers collapse to the
23800 /// last part, which is what every other name path here does (SPG is
23801 /// single-schema).
23802 fn take_comma_separated_names(&mut self) -> Vec<String> {
23803 let mut out = Vec::new();
23804 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23805 self.advance();
23806 let mut last = n;
23807 while matches!(self.peek(), Token::Dot) {
23808 self.advance();
23809 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23810 last = t;
23811 }
23812 }
23813 out.push(last);
23814 if matches!(self.peek(), Token::Comma) {
23815 self.advance();
23816 } else {
23817 break;
23818 }
23819 }
23820 out
23821 }
23822
23823 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23824 ///
23825 /// The general cast-target path tests this inline; the types with their
23826 /// own `CastTarget` variant need it as a guard on their match arm,
23827 /// which is what this exists for.
23828 fn peek_postfix_array_brackets(&self) -> bool {
23829 matches!(self.peek(), Token::LBracket)
23830 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23831 }
23832
23833 /// Parse the operator tail after a `(a, b, …)` row constructor
23834 /// and expand at parse time. `=` is the conjunction of element
23835 /// equalities; `<>` its negation; the order operators expand
23836 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23837 /// equalities. Anything else (a bare row value, a subquery
23838 /// RHS) errors honestly — SPG has no composite runtime value.
23839 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23840 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23841 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23842 lhs: Box::new(l.clone()),
23843 op: BinOp::Eq,
23844 rhs: Box::new(r.clone()),
23845 });
23846 let first = it.next().expect("row has at least two elements");
23847 it.fold(first, |acc, e| Expr::Binary {
23848 lhs: Box::new(acc),
23849 op: BinOp::And,
23850 rhs: Box::new(e),
23851 })
23852 }
23853 // Lexicographic (a,b) OP (c,d):
23854 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
23855 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23856 if lhs.len() == 1 {
23857 return Expr::Binary {
23858 lhs: Box::new(lhs[0].clone()),
23859 op: last,
23860 rhs: Box::new(rhs[0].clone()),
23861 };
23862 }
23863 let head_strict = Expr::Binary {
23864 lhs: Box::new(lhs[0].clone()),
23865 op: strict,
23866 rhs: Box::new(rhs[0].clone()),
23867 };
23868 let head_eq = Expr::Binary {
23869 lhs: Box::new(lhs[0].clone()),
23870 op: BinOp::Eq,
23871 rhs: Box::new(rhs[0].clone()),
23872 };
23873 Expr::Binary {
23874 lhs: Box::new(head_strict),
23875 op: BinOp::Or,
23876 rhs: Box::new(Expr::Binary {
23877 lhs: Box::new(head_eq),
23878 op: BinOp::And,
23879 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23880 }),
23881 }
23882 }
23883 let negated_in = if matches!(self.peek(), Token::Not)
23884 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23885 {
23886 self.advance();
23887 true
23888 } else {
23889 false
23890 };
23891 if matches!(self.peek(), Token::In) {
23892 self.advance();
23893 if !matches!(self.peek(), Token::LParen) {
23894 return Err(self.err(alloc::format!(
23895 "expected '(' after row IN, got {:?}",
23896 self.peek()
23897 )));
23898 }
23899 self.advance();
23900 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23901 // not a list of literal rows. Row-vs-list decomposes to
23902 // OR-of-AND above, but the subquery's rows are only known at
23903 // runtime, so keep it as a RowInSubquery node.
23904 if matches!(self.peek(), Token::Select) {
23905 let inner = self.parse_select_stmt()?;
23906 if !matches!(self.peek(), Token::RParen) {
23907 return Err(self.err(alloc::format!(
23908 "expected ')' after row IN-subquery, got {:?}",
23909 self.peek()
23910 )));
23911 }
23912 self.advance();
23913 let Statement::Select(s) = inner else {
23914 unreachable!("parse_select_stmt always returns Statement::Select")
23915 };
23916 return Ok(Expr::RowInSubquery {
23917 row,
23918 subquery: Box::new(s),
23919 negated: negated_in,
23920 });
23921 }
23922 let mut alternatives: Vec<Expr> = Vec::new();
23923 loop {
23924 // Optional ROW keyword before the paren row.
23925 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23926 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23927 {
23928 self.advance();
23929 }
23930 if !matches!(self.peek(), Token::LParen) {
23931 return Err(self.err(alloc::format!(
23932 "expected '(' to open a row inside IN, got {:?}",
23933 self.peek()
23934 )));
23935 }
23936 self.advance();
23937 let mut rhs = alloc::vec![self.parse_expr(0)?];
23938 while matches!(self.peek(), Token::Comma) {
23939 self.advance();
23940 rhs.push(self.parse_expr(0)?);
23941 }
23942 if !matches!(self.peek(), Token::RParen) {
23943 return Err(self.err(alloc::format!(
23944 "expected ')' after row inside IN, got {:?}",
23945 self.peek()
23946 )));
23947 }
23948 self.advance();
23949 if rhs.len() != row.len() {
23950 return Err(self.err(alloc::format!(
23951 "row IN arity mismatch: left has {}, right has {}",
23952 row.len(),
23953 rhs.len()
23954 )));
23955 }
23956 alternatives.push(row_eq(&row, &rhs));
23957 if matches!(self.peek(), Token::Comma) {
23958 self.advance();
23959 continue;
23960 }
23961 break;
23962 }
23963 if !matches!(self.peek(), Token::RParen) {
23964 return Err(self.err(alloc::format!(
23965 "expected ')' to close row IN list, got {:?}",
23966 self.peek()
23967 )));
23968 }
23969 self.advance();
23970 let mut it = alternatives.into_iter();
23971 let first = it.next().expect("IN list has at least one row");
23972 let combined = it.fold(first, |acc, e| Expr::Binary {
23973 lhs: Box::new(acc),
23974 op: BinOp::Or,
23975 rhs: Box::new(e),
23976 });
23977 return Ok(maybe_not(combined, negated_in));
23978 }
23979 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23980 // two periods share at least one time point. Each pair is
23981 // normalised with least/greatest (PG accepts the endpoints
23982 // in either order), then lowered to the standard
23983 // `start1 < end2 AND start2 < end1` form.
23984 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23985 if row.len() != 2 {
23986 return Err(self.err(alloc::format!(
23987 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23988 row.len()
23989 )));
23990 }
23991 self.advance();
23992 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23993 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23994 {
23995 self.advance();
23996 }
23997 if !matches!(self.peek(), Token::LParen) {
23998 return Err(self.err(alloc::format!(
23999 "expected '(' after OVERLAPS, got {:?}",
24000 self.peek()
24001 )));
24002 }
24003 self.advance();
24004 let r0 = self.parse_expr(0)?;
24005 if !matches!(self.peek(), Token::Comma) {
24006 return Err(self.err(alloc::format!(
24007 "OVERLAPS needs (start, end) on the right, got {:?}",
24008 self.peek()
24009 )));
24010 }
24011 self.advance();
24012 let r1 = self.parse_expr(0)?;
24013 if !matches!(self.peek(), Token::RParen) {
24014 return Err(self.err(alloc::format!(
24015 "expected ')' after OVERLAPS pair, got {:?}",
24016 self.peek()
24017 )));
24018 }
24019 self.advance();
24020 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
24021 name: String::from(name),
24022 args: alloc::vec![a.clone(), b.clone()],
24023 };
24024 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
24025 lhs: Box::new(lhs),
24026 op: BinOp::Lt,
24027 rhs: Box::new(rhs),
24028 };
24029 return Ok(Expr::Binary {
24030 lhs: Box::new(lt(
24031 pair_fn("least", &row[0], &row[1]),
24032 pair_fn("greatest", &r0, &r1),
24033 )),
24034 op: BinOp::And,
24035 rhs: Box::new(lt(
24036 pair_fn("least", &r0, &r1),
24037 pair_fn("greatest", &row[0], &row[1]),
24038 )),
24039 });
24040 }
24041 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
24042 // PG, `IS NULL` is true only when EVERY field is NULL, and
24043 // `IS NOT NULL` is true only when every field is non-NULL — the
24044 // latter is NOT the negation of the former (a mixed row is
24045 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
24046 // which reproduces exactly that all-fields semantics.
24047 if matches!(self.peek(), Token::Is) {
24048 self.advance();
24049 let negated = if matches!(self.peek(), Token::Not) {
24050 self.advance();
24051 true
24052 } else {
24053 false
24054 };
24055 if !matches!(self.peek(), Token::Null) {
24056 return Err(self.err(alloc::format!(
24057 "expected NULL after row IS [NOT], got {:?}",
24058 self.peek()
24059 )));
24060 }
24061 self.advance();
24062 let mut it = row.iter().map(|e| Expr::IsNull {
24063 expr: Box::new(e.clone()),
24064 negated,
24065 });
24066 let first = it.next().expect("row has at least two elements");
24067 return Ok(it.fold(first, |acc, e| Expr::Binary {
24068 lhs: Box::new(acc),
24069 op: BinOp::And,
24070 rhs: Box::new(e),
24071 }));
24072 }
24073 let op = match self.peek() {
24074 Token::Eq => BinOp::Eq,
24075 Token::NotEq => BinOp::NotEq,
24076 Token::Lt => BinOp::Lt,
24077 Token::LtEq => BinOp::LtEq,
24078 Token::Gt => BinOp::Gt,
24079 Token::GtEq => BinOp::GtEq,
24080 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
24081 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
24082 // constructor value, identical to the `ROW(a, b, …)` keyword form:
24083 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
24084 // (`::text`, `.field`) applies at the caller just as it does for the
24085 // ROW(...) node. All the comparison / predicate forms returned above.
24086 _ => {
24087 return Ok(Expr::FunctionCall {
24088 name: String::from("row"),
24089 args: row,
24090 });
24091 }
24092 };
24093 self.advance();
24094 // Optional ROW keyword before the paren row.
24095 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
24096 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
24097 {
24098 self.advance();
24099 }
24100 if !matches!(self.peek(), Token::LParen) {
24101 return Err(self.err(alloc::format!(
24102 "expected '(' to open the right-hand row, got {:?}",
24103 self.peek()
24104 )));
24105 }
24106 self.advance();
24107 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
24108 // subquery. Kept as a node (the subquery's row is a runtime value);
24109 // the literal-RHS form below still decomposes at parse time.
24110 if matches!(self.peek(), Token::Select) {
24111 let inner = self.parse_select_stmt()?;
24112 if !matches!(self.peek(), Token::RParen) {
24113 return Err(self.err(alloc::format!(
24114 "expected ')' after row comparison subquery, got {:?}",
24115 self.peek()
24116 )));
24117 }
24118 self.advance();
24119 let Statement::Select(s) = inner else {
24120 unreachable!("parse_select_stmt always returns Statement::Select")
24121 };
24122 return Ok(Expr::RowCmpSubquery {
24123 row,
24124 op,
24125 subquery: Box::new(s),
24126 });
24127 }
24128 let mut rhs = alloc::vec![self.parse_expr(0)?];
24129 while matches!(self.peek(), Token::Comma) {
24130 self.advance();
24131 rhs.push(self.parse_expr(0)?);
24132 }
24133 if !matches!(self.peek(), Token::RParen) {
24134 return Err(self.err(alloc::format!(
24135 "expected ')' after right-hand row, got {:?}",
24136 self.peek()
24137 )));
24138 }
24139 self.advance();
24140 if rhs.len() != row.len() {
24141 // v7.39 (round 239) — PG's wording (42601).
24142 return Err(self.err("unequal number of entries in row expressions".to_string()));
24143 }
24144 Ok(match op {
24145 BinOp::Eq => row_eq(&row, &rhs),
24146 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
24147 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
24148 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
24149 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
24150 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
24151 _ => unreachable!("op restricted above"),
24152 })
24153 }
24154
24155 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
24156 /// escape character becomes the matcher's default backslash:
24157 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
24158 /// → the char itself, and any pre-existing backslash escapes
24159 /// itself so it stays literal. Both operands must be string
24160 /// literals — a runtime pattern would need matcher support.
24161 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24162 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24163 (&pattern, &esc)
24164 else {
24165 return Err(
24166 "LIKE ... ESCAPE requires string-literal pattern and escape \
24167 (runtime escape characters are not supported yet)"
24168 .into(),
24169 );
24170 };
24171 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24172 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24173 // multi-character escape is an error.
24174 let esc_ch: Option<char> = {
24175 let mut ch_iter = e.chars();
24176 match (ch_iter.next(), ch_iter.next()) {
24177 (Some(c), None) => Some(c),
24178 (None, _) => None,
24179 (Some(_), Some(_)) => {
24180 return Err(alloc::format!(
24181 "ESCAPE must be a single character, got {e:?}"
24182 ));
24183 }
24184 }
24185 };
24186 let mut out = String::with_capacity(p.len() + 4);
24187 let mut chars = p.chars();
24188 while let Some(c) = chars.next() {
24189 if Some(c) == esc_ch {
24190 match chars.next() {
24191 // Escaped wildcard / escaped escape → keep the
24192 // next char literal via backslash.
24193 Some(next) => {
24194 out.push('\\');
24195 out.push(next);
24196 }
24197 None => {
24198 return Err("LIKE pattern ends with the escape character".into());
24199 }
24200 }
24201 } else if c == '\\' && esc_ch != Some('\\') {
24202 // A raw backslash is literal under a custom (or absent) escape
24203 // — escape it for the backslash-based matcher.
24204 out.push('\\');
24205 out.push('\\');
24206 } else {
24207 out.push(c);
24208 }
24209 }
24210 Ok(Expr::Literal(Literal::String(out)))
24211 }
24212
24213 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24214 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24215 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24216 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24217 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24218 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24219 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24220 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24221 /// array expression errors honestly rather than silently mismatching.
24222 fn try_like_any_all(
24223 &mut self,
24224 base: &Expr,
24225 negated: bool,
24226 case_insensitive: bool,
24227 ) -> Result<Option<Expr>, ParseError> {
24228 let is_any = match self.peek() {
24229 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24230 Token::Ident(s)
24231 if s.eq_ignore_ascii_case("any")
24232 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24233 {
24234 true
24235 }
24236 _ => return Ok(None),
24237 };
24238 self.advance(); // ANY / ALL
24239 self.advance(); // '('
24240 let arr = self.parse_expr(0)?;
24241 if !matches!(self.peek(), Token::RParen) {
24242 return Err(self.err(format!(
24243 "expected ')' after LIKE {} argument, got {:?}",
24244 if is_any { "ANY" } else { "ALL" },
24245 self.peek()
24246 )));
24247 }
24248 self.advance(); // ')'
24249 let Expr::Array(items) = arr else {
24250 return Err(self.err(
24251 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24252 ));
24253 };
24254 let mut clauses = items.into_iter().map(|p| Expr::Like {
24255 expr: Box::new(base.clone()),
24256 pattern: Box::new(p),
24257 negated,
24258 case_insensitive,
24259 });
24260 let Some(first) = clauses.next() else {
24261 // ANY(empty) = FALSE, ALL(empty) = TRUE.
24262 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24263 };
24264 let op = if is_any { BinOp::Or } else { BinOp::And };
24265 let combined = clauses.fold(first, |acc, c| Expr::Binary {
24266 lhs: Box::new(acc),
24267 op,
24268 rhs: Box::new(c),
24269 });
24270 Ok(Some(combined))
24271 }
24272
24273 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
24274 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24275 /// `AND` is not swallowed.
24276 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24277 self.advance(); // BETWEEN
24278 // SYMMETRIC — the bounds may arrive in either order; both
24279 // orientations OR together. ASYMMETRIC is the default and
24280 // absorbs as noise.
24281 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24282 {
24283 self.advance();
24284 true
24285 } else {
24286 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24287 self.advance();
24288 }
24289 false
24290 };
24291 let low = self.parse_expr(6)?;
24292 if !matches!(self.peek(), Token::And) {
24293 return Err(self.err(format!(
24294 "expected AND after BETWEEN low bound, got {:?}",
24295 self.peek()
24296 )));
24297 }
24298 self.advance();
24299 let high = self.parse_expr(6)?;
24300 let target = Box::new(expr);
24301 let range = |lo: Expr, hi: Expr| Expr::Binary {
24302 lhs: Box::new(Expr::Binary {
24303 lhs: target.clone(),
24304 op: BinOp::GtEq,
24305 rhs: Box::new(lo),
24306 }),
24307 op: BinOp::And,
24308 rhs: Box::new(Expr::Binary {
24309 lhs: target.clone(),
24310 op: BinOp::LtEq,
24311 rhs: Box::new(hi),
24312 }),
24313 };
24314 let combined = if symmetric {
24315 Expr::Binary {
24316 lhs: Box::new(range(low.clone(), high.clone())),
24317 op: BinOp::Or,
24318 rhs: Box::new(range(high, low)),
24319 }
24320 } else {
24321 range(low, high)
24322 };
24323 Ok(maybe_not(combined, negated))
24324 }
24325
24326 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
24327 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24328 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24329 /// Caller already consumed the leading `WITH` ident.
24330 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24331 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24332 /// self-reference that appears more than once in a single term.
24333 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24334 use crate::ast::{CteBody, SelectStatement};
24335 if !cte.recursive {
24336 return Ok(());
24337 }
24338 let CteBody::Select(body) = &cte.body else {
24339 return Ok(());
24340 };
24341 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24342 // check the anchor and every peer term.
24343 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24344 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24345 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24346 return Err(self.err(String::from(
24347 "ORDER BY in a recursive query is not implemented",
24348 )));
24349 }
24350 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24351 return Err(self.err(String::from(
24352 "LIMIT in a recursive query is not implemented",
24353 )));
24354 }
24355 let self_refs = |s: &SelectStatement| -> usize {
24356 let Some(from) = &s.from else {
24357 return 0;
24358 };
24359 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24360 for j in &from.joins {
24361 if j.table.name.eq_ignore_ascii_case(&cte.name) {
24362 n += 1;
24363 }
24364 }
24365 n
24366 };
24367 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24368 return Err(self.err(alloc::format!(
24369 "recursive reference to query \"{}\" must not appear more than once",
24370 cte.name
24371 )));
24372 }
24373 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24374 // apply only when the body actually references itself (a non-self-
24375 // referencing CTE under WITH RECURSIVE may use any set-op shape).
24376 let anchor_refs = self_refs(body);
24377 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24378 if anchor_refs > 0 || union_refs {
24379 // Shape: the top level must be UNION [ALL] arms only. A self-ref
24380 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24381 // "does not have the form" error — SPG used to compute a value.
24382 if body.unions.is_empty()
24383 || body.unions.iter().any(|(k, _)| {
24384 !matches!(
24385 k,
24386 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24387 )
24388 })
24389 {
24390 return Err(self.err(alloc::format!(
24391 "recursive query \"{}\" does not have the form non-recursive-term \
24392 UNION [ALL] recursive-term",
24393 cte.name
24394 )));
24395 }
24396 if anchor_refs > 0 {
24397 return Err(self.err(alloc::format!(
24398 "recursive reference to query \"{}\" must not appear within its non-recursive term",
24399 cte.name
24400 )));
24401 }
24402 }
24403 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24404 for (_, u) in &body.unions {
24405 if self_refs(u) == 0 {
24406 continue;
24407 }
24408 // The self-reference must not sit on the nullable side of an outer
24409 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24410 if let Some(from) = &u.from {
24411 for (i, j) in from.joins.iter().enumerate() {
24412 let left_has_self = is_self(&from.primary)
24413 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24414 let violated = match j.kind {
24415 crate::ast::JoinKind::Left => is_self(&j.table),
24416 crate::ast::JoinKind::Right => left_has_self,
24417 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24418 _ => false,
24419 };
24420 if violated {
24421 return Err(self.err(alloc::format!(
24422 "recursive reference to query \"{}\" must not appear within an outer join",
24423 cte.name
24424 )));
24425 }
24426 }
24427 }
24428 // No aggregates at the top level of the recursive term (SPG used
24429 // to run them and surface a misleading downstream error).
24430 let mut items_and_having: Vec<&Expr> = Vec::new();
24431 for it in &u.items {
24432 if let crate::ast::SelectItem::Expr { expr, .. } = it {
24433 items_and_having.push(expr);
24434 }
24435 }
24436 if let Some(h) = &u.having {
24437 items_and_having.push(h);
24438 }
24439 for e in items_and_having {
24440 if expr_has_toplevel_aggregate(e) {
24441 return Err(self.err(String::from(
24442 "aggregate functions are not allowed in a recursive query's recursive term",
24443 )));
24444 }
24445 }
24446 }
24447 // A self-reference inside a sublink expression (EXISTS / IN / scalar
24448 // subquery) anywhere in the body is rejected; a plain FROM derived
24449 // table is legal in PG and untouched here.
24450 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24451 all_terms.extend(body.unions.iter().map(|(_, u)| u));
24452 for term in all_terms {
24453 if select_has_self_ref_in_sublink(term, &cte.name) {
24454 return Err(self.err(alloc::format!(
24455 "recursive reference to query \"{}\" must not appear within a subquery",
24456 cte.name
24457 )));
24458 }
24459 }
24460 Ok(())
24461 }
24462
24463 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24464 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24465 /// right after parse so the engine sees a plain recursive CTE with the
24466 /// tracking columns already projected. DEPTH FIRST and CYCLE are
24467 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24468 /// text-rendered rows can't provide, and errors honestly.
24469 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24470 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24471 if cte.search.is_none() && cte.cycle.is_none() {
24472 return Ok(());
24473 }
24474 let cte_name = cte.name.clone();
24475 let col_names = cte.column_overrides.clone();
24476 if col_names.is_empty() {
24477 return Err(
24478 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24479 );
24480 }
24481 let search = cte.search.take();
24482 let cycle = cte.cycle.take();
24483 let mut extra_cols: Vec<String> = Vec::new();
24484 let col_ref = |name: &str| {
24485 Expr::Column(ColumnName {
24486 qualifier: Some(cte_name.clone()),
24487 name: name.to_string(),
24488 })
24489 };
24490 // Position of a SEARCH/CYCLE column within the CTE's column list.
24491 let pos_of = |name: &str| -> Result<usize, ParseError> {
24492 col_names
24493 .iter()
24494 .position(|c| c.eq_ignore_ascii_case(name))
24495 .ok_or_else(|| {
24496 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24497 })
24498 };
24499 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24500 let mut args = Vec::with_capacity(positions.len());
24501 for &p in positions {
24502 match items.get(p) {
24503 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24504 _ => {
24505 return Err(self.err(
24506 "SEARCH/CYCLE column maps to a non-expression select item".into(),
24507 ));
24508 }
24509 }
24510 }
24511 Ok(Expr::FunctionCall {
24512 name: "row".into(),
24513 args,
24514 })
24515 };
24516 let CteBody::Select(body) = &mut cte.body else {
24517 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24518 };
24519 if body.unions.is_empty() {
24520 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24521 }
24522 let rec = body.unions.len() - 1; // recursive term = last UNION peer
24523
24524 if let Some(srch) = search {
24525 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24526 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24527 // no typed `record[]`, but element-wise array ORDER BY is correct
24528 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24529 // exactly onto a typed array: DEPTH is the root→node path
24530 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24531 // orders numerically (multi-digit keys included), matching PG.
24532 //
24533 // A multi-column BY would need a record[] to keep the per-node key
24534 // tuple orderable, which SPG can't express — error honestly there
24535 // rather than mis-order.
24536 if srch.by_columns.len() != 1 {
24537 return Err(self.err(
24538 "SEARCH … BY with multiple columns needs typed record[] ordering \
24539 SPG doesn't have yet; a single BY column is supported"
24540 .into(),
24541 ));
24542 }
24543 let key_pos = pos_of(&srch.by_columns[0])?;
24544 let base_key = match body.items.get(key_pos) {
24545 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24546 _ => {
24547 return Err(
24548 self.err("SEARCH BY column maps to a non-expression select item".into())
24549 );
24550 }
24551 };
24552 let rec_key = match body.unions[rec].1.items.get(key_pos) {
24553 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24554 _ => {
24555 return Err(
24556 self.err("SEARCH BY column maps to a non-expression select item".into())
24557 );
24558 }
24559 };
24560 if srch.depth_first {
24561 // base: ARRAY[key]; rec: array_append(cte.set, key).
24562 body.items.push(SelectItem::Expr {
24563 expr: Expr::Array(alloc::vec![base_key]),
24564 alias: Some(srch.set_column.clone()),
24565 });
24566 body.unions[rec].1.items.push(SelectItem::Expr {
24567 expr: Expr::FunctionCall {
24568 name: "array_append".into(),
24569 args: alloc::vec![col_ref(&srch.set_column), rec_key],
24570 },
24571 alias: Some(srch.set_column.clone()),
24572 });
24573 } else {
24574 // BREADTH: [depth, key]; depth starts at 0 and increments. The
24575 // leading depth element dominates the element-wise comparison,
24576 // so shallower rows sort first, then by key — PG's (depth, key).
24577 body.items.push(SelectItem::Expr {
24578 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24579 alias: Some(srch.set_column.clone()),
24580 });
24581 // rec depth = cte.set[1] + 1.
24582 let parent_depth = Expr::ArraySubscript {
24583 target: Box::new(col_ref(&srch.set_column)),
24584 index: Box::new(Expr::Literal(Literal::Integer(1))),
24585 };
24586 body.unions[rec].1.items.push(SelectItem::Expr {
24587 expr: Expr::Array(alloc::vec![
24588 Expr::Binary {
24589 lhs: Box::new(parent_depth),
24590 op: BinOp::Add,
24591 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24592 },
24593 rec_key,
24594 ]),
24595 alias: Some(srch.set_column.clone()),
24596 });
24597 }
24598 extra_cols.push(srch.set_column);
24599 }
24600
24601 if let Some(cyc) = cycle {
24602 let positions: Vec<usize> = cyc
24603 .columns
24604 .iter()
24605 .map(|c| pos_of(c))
24606 .collect::<Result<_, _>>()?;
24607 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24608 // cast it to text for the cycle path: membership only needs equality,
24609 // and the record text form gives SPG a TextArray path (SPG has no
24610 // typed record[] array). Cycle detection is unaffected.
24611 let base_row = Expr::Cast {
24612 expr: Box::new(row_of(&body.items, &positions)?),
24613 target: CastTarget::Text,
24614 };
24615 let rec_row = Expr::Cast {
24616 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24617 target: CastTarget::Text,
24618 };
24619 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24620 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24621 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24622 body.items.push(SelectItem::Expr {
24623 expr: Expr::Literal(dflt.clone()),
24624 alias: Some(cyc.mark_column.clone()),
24625 });
24626 body.items.push(SelectItem::Expr {
24627 expr: Expr::Array(alloc::vec![base_row]),
24628 alias: Some(cyc.path_column.clone()),
24629 });
24630 // rec mark: ROW(cols) already in the path → cycle.
24631 let hit = Expr::AnyAll {
24632 expr: Box::new(rec_row.clone()),
24633 op: BinOp::Eq,
24634 array: Box::new(col_ref(&cyc.path_column)),
24635 is_any: true,
24636 };
24637 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24638 Expr::Case {
24639 operand: None,
24640 branches: alloc::vec![(hit, Expr::Literal(mark))],
24641 else_branch: Some(Box::new(Expr::Literal(dflt))),
24642 }
24643 } else {
24644 hit
24645 };
24646 body.unions[rec].1.items.push(SelectItem::Expr {
24647 expr: mark_expr,
24648 alias: Some(cyc.mark_column.clone()),
24649 });
24650 // rec path: array_append(cte.path, ROW(cols)).
24651 body.unions[rec].1.items.push(SelectItem::Expr {
24652 expr: Expr::FunctionCall {
24653 name: "array_append".into(),
24654 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24655 },
24656 alias: Some(cyc.path_column.clone()),
24657 });
24658 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24659 let stop = Expr::Unary {
24660 op: UnOp::Not,
24661 expr: Box::new(col_ref(&cyc.mark_column)),
24662 };
24663 let w = &mut body.unions[rec].1.where_;
24664 *w = Some(match w.take() {
24665 Some(prev) => Expr::Binary {
24666 lhs: Box::new(prev),
24667 op: BinOp::And,
24668 rhs: Box::new(stop),
24669 },
24670 None => stop,
24671 });
24672 extra_cols.push(cyc.mark_column);
24673 extra_cols.push(cyc.path_column);
24674 }
24675 cte.column_overrides.extend(extra_cols);
24676 Ok(())
24677 }
24678
24679 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24680 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24681 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24682 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24683 return Ok(None);
24684 }
24685 self.advance(); // SEARCH
24686 let depth_first = match self.peek() {
24687 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24688 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24689 other => {
24690 return Err(self.err(format!(
24691 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24692 )));
24693 }
24694 };
24695 self.advance();
24696 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24697 return Err(self.err(format!(
24698 "expected FIRST after SEARCH mode, got {:?}",
24699 self.peek()
24700 )));
24701 }
24702 self.advance();
24703 if !self.peek_is_by() {
24704 return Err(self.err(format!(
24705 "expected BY after SEARCH … FIRST, got {:?}",
24706 self.peek()
24707 )));
24708 }
24709 self.advance();
24710 let mut by_columns = alloc::vec![self.expect_ident_like()?];
24711 while matches!(self.peek(), Token::Comma) {
24712 self.advance();
24713 by_columns.push(self.expect_ident_like()?);
24714 }
24715 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24716 return Err(self.err(format!(
24717 "expected SET in SEARCH clause, got {:?}",
24718 self.peek()
24719 )));
24720 }
24721 self.advance();
24722 let set_column = self.expect_ident_like()?;
24723 Ok(Some(crate::ast::SearchClause {
24724 depth_first,
24725 by_columns,
24726 set_column,
24727 }))
24728 }
24729
24730 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24731 /// USING pathcol`. Returns None when the next token isn't CYCLE.
24732 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24733 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24734 return Ok(None);
24735 }
24736 self.advance(); // CYCLE
24737 let mut columns = alloc::vec![self.expect_ident_like()?];
24738 while matches!(self.peek(), Token::Comma) {
24739 self.advance();
24740 columns.push(self.expect_ident_like()?);
24741 }
24742 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24743 return Err(self.err(format!(
24744 "expected SET in CYCLE clause, got {:?}",
24745 self.peek()
24746 )));
24747 }
24748 self.advance();
24749 let mark_column = self.expect_ident_like()?;
24750 let mut mark_value = None;
24751 let mut default_value = None;
24752 if matches!(self.peek(), Token::To) {
24753 self.advance();
24754 mark_value = Some(self.parse_cycle_literal()?);
24755 if !matches!(self.peek(), Token::Default) {
24756 return Err(self.err(format!(
24757 "expected DEFAULT after CYCLE … TO, got {:?}",
24758 self.peek()
24759 )));
24760 }
24761 self.advance();
24762 default_value = Some(self.parse_cycle_literal()?);
24763 }
24764 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24765 return Err(self.err(format!(
24766 "expected USING in CYCLE clause, got {:?}",
24767 self.peek()
24768 )));
24769 }
24770 self.advance();
24771 let path_column = self.expect_ident_like()?;
24772 Ok(Some(crate::ast::CycleClause {
24773 columns,
24774 mark_column,
24775 mark_value,
24776 default_value,
24777 path_column,
24778 }))
24779 }
24780
24781 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24782 /// literal (string / bool / number) in PG.
24783 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24784 match self.parse_expr(0)? {
24785 Expr::Literal(l) => Ok(l),
24786 other => Err(self.err(format!(
24787 "CYCLE mark/default value must be a literal, got {other:?}"
24788 ))),
24789 }
24790 }
24791
24792 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24793 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24794 // Comes through as an identifier; consume it if present and
24795 // mark every CTE in the clause as recursive (PG semantics —
24796 // the flag is per-WITH, not per-CTE).
24797 let mut recursive = false;
24798 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24799 && s.eq_ignore_ascii_case("recursive")
24800 {
24801 self.advance();
24802 recursive = true;
24803 }
24804 let mut ctes = Vec::new();
24805 loop {
24806 let name = self.expect_ident_like()?;
24807 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24808 // PG uses these to rename the body's output columns; we
24809 // do the same below by overriding `columns[i].name`.
24810 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24811 self.advance();
24812 let mut names = Vec::new();
24813 loop {
24814 names.push(self.expect_ident_like()?);
24815 if matches!(self.peek(), Token::Comma) {
24816 self.advance();
24817 continue;
24818 }
24819 break;
24820 }
24821 if !matches!(self.peek(), Token::RParen) {
24822 return Err(self.err(format!(
24823 "expected ')' to close CTE column list, got {:?}",
24824 self.peek()
24825 )));
24826 }
24827 self.advance();
24828 names
24829 } else {
24830 Vec::new()
24831 };
24832 // AS is a reserved Token::As (used by SELECT-item / FROM
24833 // aliasing) — handle it specially rather than as a bare
24834 // ident.
24835 if !matches!(self.peek(), Token::As) {
24836 return Err(self.err(format!(
24837 "expected AS after CTE name {name:?}, got {:?}",
24838 self.peek()
24839 )));
24840 }
24841 self.advance();
24842 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24843 // MATERIALIZED` optimizer hints. SPG materialises every
24844 // CTE, so both spellings are accepted and absorbed.
24845 if matches!(self.peek(), Token::Not) {
24846 self.advance(); // NOT
24847 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24848 if s.eq_ignore_ascii_case("materialized"))
24849 {
24850 self.advance();
24851 } else {
24852 return Err(self.err(format!(
24853 "expected MATERIALIZED after AS NOT, got {:?}",
24854 self.peek()
24855 )));
24856 }
24857 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24858 if s.eq_ignore_ascii_case("materialized"))
24859 {
24860 self.advance();
24861 }
24862 if !matches!(self.peek(), Token::LParen) {
24863 return Err(self.err(format!(
24864 "expected '(' after AS in WITH clause, got {:?}",
24865 self.peek()
24866 )));
24867 }
24868 self.advance();
24869 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24870 // RETURNING) as the CTE body in addition to SELECT.
24871 // PG writable CTE semantics. UPDATE / DELETE come in as
24872 // bare Idents (lexer keeps SELECT / INSERT as reserved
24873 // tokens but treats the rest of DML as case-insensitive
24874 // idents).
24875 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24876 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24877 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24878 let body = match self.peek() {
24879 Token::Select => {
24880 let inner = self.parse_select_stmt()?;
24881 let Statement::Select(s) = inner else {
24882 unreachable!("parse_select_stmt returns Select");
24883 };
24884 crate::ast::CteBody::Select(s)
24885 }
24886 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24887 // `SELECT * FROM t` this way and accepts it wherever a
24888 // SELECT goes, so the CTE body dispatch needs its own
24889 // arm: this match is keyed on the FIRST token, and
24890 // `Token::Table` fell through to a tail that then
24891 // rejected what it got. `parse_table_shorthand` has
24892 // returned a desugared SelectStatement since the
24893 // shorthand landed — only the routing was missing.
24894 // Round 868 found this by putting the shorthand in a
24895 // subquery; every earlier check used a top-level form.
24896 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24897 // `SELECT * FROM t` this way and accepts it wherever a
24898 // SELECT goes, so the CTE body dispatch needs its own
24899 // arm: this match is keyed on the FIRST token, and
24900 // `Token::Table` fell through to a tail that rejected
24901 // what it got. `parse_table_shorthand` has returned a
24902 // desugared SelectStatement since the shorthand landed —
24903 // only the routing was missing, here and in the derived
24904 // table's second-token gate. Round 868 found both by
24905 // putting the shorthand in a subquery; every earlier
24906 // check had used a top-level form.
24907 Token::Table
24908 if matches!(
24909 self.tokens.get(self.pos + 1),
24910 Some(Token::Ident(_) | Token::QuotedIdent(_))
24911 ) =>
24912 {
24913 let mut head = self.parse_table_shorthand()?;
24914 self.parse_setop_chain_into(&mut head)?;
24915 self.parse_select_tail_into(&mut head)?;
24916 crate::ast::CteBody::Select(head)
24917 }
24918 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24919 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24920 // the shared rows helper onto a Select body.
24921 Token::Values => {
24922 self.advance(); // VALUES
24923 let mut head = self.parse_values_rows_body()?;
24924 // A VALUES seed can head a set-operation chain —
24925 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24926 // SELECT n+1 FROM t …). Attach any trailing
24927 // UNION / INTERSECT / EXCEPT peers so the
24928 // recursive-CTE body parses like the SELECT seed.
24929 self.parse_setop_chain_into(&mut head)?;
24930 crate::ast::CteBody::Select(head)
24931 }
24932 Token::Insert => {
24933 let inner = self.parse_one_statement()?;
24934 let Statement::Insert(s) = inner else {
24935 unreachable!("Token::Insert routes to Insert");
24936 };
24937 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24938 }
24939 _ if is_update_kw => {
24940 let inner = self.parse_one_statement()?;
24941 let Statement::Update(s) = inner else {
24942 return Err(
24943 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24944 );
24945 };
24946 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24947 }
24948 _ if is_delete_kw => {
24949 let inner = self.parse_one_statement()?;
24950 let Statement::Delete(s) = inner else {
24951 return Err(
24952 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24953 );
24954 };
24955 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24956 }
24957 // v7.39 (round 149) — PG 17 allows MERGE as a
24958 // data-modifying CTE body.
24959 _ if is_merge_kw => {
24960 let inner = self.parse_one_statement()?;
24961 let Statement::Merge(s) = inner else {
24962 return Err(
24963 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24964 );
24965 };
24966 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24967 }
24968 // v7.39 (round 151) — a CTE body may itself be
24969 // WITH-headed (PG grammar: PreparableStmt carries its
24970 // own with_clause). The nested statement keeps its own
24971 // ctes; the modifying-CTE-at-top-level rule is enforced
24972 // at execution.
24973 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24974 self.advance(); // WITH
24975 match self.parse_with_cte_then_select()? {
24976 Statement::Select(s) => crate::ast::CteBody::Select(s),
24977 Statement::Insert(s) => {
24978 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24979 }
24980 Statement::Update(s) => {
24981 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24982 }
24983 Statement::Delete(s) => {
24984 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24985 }
24986 Statement::Merge(s) => {
24987 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24988 }
24989
24990 other => {
24991 return Err(self.err(format!(
24992 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24993 )));
24994 }
24995 }
24996 }
24997 other => {
24998 return Err(self.err(format!(
24999 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
25000 )));
25001 }
25002 };
25003 if !matches!(self.peek(), Token::RParen) {
25004 return Err(self.err(format!(
25005 "expected ')' after CTE body, got {:?}",
25006 self.peek()
25007 )));
25008 }
25009 self.advance();
25010 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
25011 // CTE, desugared into extra body columns by the engine.
25012 let search = self.parse_cte_search_clause()?;
25013 let cycle = self.parse_cte_cycle_clause()?;
25014 let mut cte = crate::ast::Cte {
25015 name,
25016 body,
25017 recursive,
25018 column_overrides,
25019 search,
25020 cycle,
25021 };
25022 self.validate_recursive_cte(&cte)?;
25023 self.desugar_cte_search_cycle(&mut cte)?;
25024 ctes.push(cte);
25025 if matches!(self.peek(), Token::Comma) {
25026 self.advance();
25027 continue;
25028 }
25029 break;
25030 }
25031 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
25032 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
25033 // the parsed CTEs to whichever statement the body produces.
25034 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
25035 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
25036 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
25037 match self.peek() {
25038 Token::Select => {
25039 let body_stmt = self.parse_select_stmt()?;
25040 let Statement::Select(mut body) = body_stmt else {
25041 unreachable!()
25042 };
25043 body.ctes = ctes;
25044 Ok(Statement::Select(body))
25045 }
25046 Token::Insert => {
25047 let body_stmt = self.parse_one_statement()?;
25048 let Statement::Insert(mut body) = body_stmt else {
25049 unreachable!()
25050 };
25051 body.ctes = ctes;
25052 Ok(Statement::Insert(body))
25053 }
25054 _ if outer_is_update => {
25055 let body_stmt = self.parse_one_statement()?;
25056 let Statement::Update(mut body) = body_stmt else {
25057 return Err(self.err(format!("expected UPDATE after WITH clause")));
25058 };
25059 body.ctes = ctes;
25060 Ok(Statement::Update(body))
25061 }
25062 _ if outer_is_delete => {
25063 let body_stmt = self.parse_one_statement()?;
25064 let Statement::Delete(mut body) = body_stmt else {
25065 return Err(self.err(format!("expected DELETE after WITH clause")));
25066 };
25067 body.ctes = ctes;
25068 Ok(Statement::Delete(body))
25069 }
25070 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
25071 // WITH RECURSIVE is rejected with PG's exact message
25072 // (parse analysis, transformWithClause).
25073 _ if outer_is_merge => {
25074 if recursive {
25075 return Err(self.err(String::from(
25076 "WITH RECURSIVE is not supported for MERGE statement",
25077 )));
25078 }
25079 let body_stmt = self.parse_one_statement()?;
25080 let Statement::Merge(mut body) = body_stmt else {
25081 return Err(self.err(format!("expected MERGE after WITH clause")));
25082 };
25083 body.ctes = ctes;
25084 Ok(Statement::Merge(body))
25085 }
25086 other => Err(self.err(format!(
25087 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
25088 ))),
25089 }
25090 }
25091
25092 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
25093 /// already consumed the leading `EXISTS` ident via
25094 /// `self.advance()`.
25095 /// v7.13.0 — parse the rest of a `CASE … END` expression after
25096 /// the leading `CASE` ident has been consumed (mailrs round-5
25097 /// G9). Supports both the searched form
25098 /// (`CASE WHEN cond THEN val …`) and the simple form
25099 /// (`CASE operand WHEN val THEN val …`).
25100 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
25101 // Disambiguate searched vs simple form: if the next token
25102 // is `WHEN`, we're in the searched form. Otherwise the
25103 // intervening expression is the operand.
25104 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
25105 None
25106 } else {
25107 Some(Box::new(self.parse_expr(0)?))
25108 };
25109 let mut branches: Vec<(Expr, Expr)> = Vec::new();
25110 loop {
25111 match self.peek() {
25112 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
25113 self.advance();
25114 let cond = self.parse_expr(0)?;
25115 match self.peek() {
25116 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
25117 self.advance();
25118 }
25119 other => {
25120 return Err(self.err(alloc::format!(
25121 "expected THEN after CASE WHEN <expr>, got {other:?}"
25122 )));
25123 }
25124 }
25125 let value = self.parse_expr(0)?;
25126 branches.push((cond, value));
25127 }
25128 _ => break,
25129 }
25130 }
25131 if branches.is_empty() {
25132 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
25133 }
25134 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
25135 {
25136 self.advance();
25137 Some(Box::new(self.parse_expr(0)?))
25138 } else {
25139 None
25140 };
25141 match self.peek() {
25142 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
25143 self.advance();
25144 }
25145 other => {
25146 return Err(self.err(alloc::format!(
25147 "expected END to close CASE expression, got {other:?}"
25148 )));
25149 }
25150 }
25151 Ok(Expr::Case {
25152 operand,
25153 branches,
25154 else_branch,
25155 })
25156 }
25157
25158 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
25159 /// query-source position (EXISTS / IN / INSERT source / CTE body /
25160 /// view body). Caller consumed the WITH keyword. Only a SELECT
25161 /// outer is grammatical here; the data-modifying-CTE-at-top-level
25162 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25163 /// maps correctly.
25164 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25165 let inner = self.parse_with_cte_then_select()?;
25166 match inner {
25167 Statement::Select(s) => Ok(s),
25168 other => Err(self.err(format!(
25169 "expected SELECT after WITH in a subquery, got {other:?}"
25170 ))),
25171 }
25172 }
25173
25174 /// True when the next token is the (unquoted) WITH keyword. WITH is
25175 /// reserved in PG, so a bare `with` can never be a column reference
25176 /// in these positions; a quoted `"with"` stays an identifier.
25177 fn peek_is_with_kw(&self) -> bool {
25178 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25179 }
25180
25181 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25182 /// `#[inline(never)]` keeps the large SelectStatement temporaries
25183 /// off parse_expr's recursive frame (the nesting-budget stack
25184 /// cliff — see the round-153 gate regression).
25185 #[inline(never)]
25186 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25187 if self.peek_is_with_kw() {
25188 self.advance();
25189 self.parse_nested_with_select()
25190 } else {
25191 match self.parse_select_stmt()? {
25192 Statement::Select(s) => Ok(s),
25193 other => Err(self.err(alloc::format!(
25194 "expected SELECT inside ANY/ALL, got {other:?}"
25195 ))),
25196 }
25197 }
25198 }
25199
25200 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25201 if !matches!(self.peek(), Token::LParen) {
25202 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25203 }
25204 self.advance();
25205 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25206 let s = if self.peek_is_with_kw() {
25207 self.advance();
25208 self.parse_nested_with_select()?
25209 } else {
25210 let inner = self.parse_select_stmt()?;
25211 let Statement::Select(s) = inner else {
25212 unreachable!("parse_select_stmt returns Select")
25213 };
25214 s
25215 };
25216 if !matches!(self.peek(), Token::RParen) {
25217 return Err(self.err(format!(
25218 "expected ')' after EXISTS-subquery, got {:?}",
25219 self.peek()
25220 )));
25221 }
25222 self.advance();
25223 Ok(Expr::Exists {
25224 subquery: Box::new(s),
25225 negated,
25226 })
25227 }
25228
25229 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25230 self.advance(); // IN
25231 if !matches!(self.peek(), Token::LParen) {
25232 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25233 }
25234 self.advance();
25235 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25236 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25237 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25238 let s = if self.peek_is_with_kw() {
25239 self.advance();
25240 self.parse_nested_with_select()?
25241 } else {
25242 let inner = self.parse_select_stmt()?;
25243 let Statement::Select(s) = inner else {
25244 unreachable!("parse_select_stmt always returns Statement::Select")
25245 };
25246 s
25247 };
25248 if !matches!(self.peek(), Token::RParen) {
25249 return Err(self.err(format!(
25250 "expected ')' after IN-subquery, got {:?}",
25251 self.peek()
25252 )));
25253 }
25254 self.advance();
25255 return Ok(Expr::InSubquery {
25256 expr: Box::new(expr),
25257 subquery: Box::new(s),
25258 negated,
25259 });
25260 }
25261 let mut elements = Vec::new();
25262 if !matches!(self.peek(), Token::RParen) {
25263 loop {
25264 elements.push(self.parse_expr(0)?);
25265 match self.peek() {
25266 Token::Comma => {
25267 self.advance();
25268 }
25269 Token::RParen => break,
25270 other => {
25271 return Err(
25272 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25273 );
25274 }
25275 }
25276 }
25277 }
25278 self.advance(); // ')'
25279 // v7.30.2 (mailrs round-25) — flat InList node instead of a
25280 // left-deep OR-Eq chain: chain depth scaled with the element
25281 // count and overflowed the stack (eval + drop are recursive).
25282 if elements.is_empty() {
25283 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25284 }
25285 Ok(Expr::InList {
25286 expr: Box::new(expr),
25287 list: elements,
25288 negated,
25289 })
25290 }
25291
25292 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25293 /// already consumed by the caller. Elements must be numeric literals
25294 /// (with optional unary `-`); any compound expression is rejected at
25295 /// parse time so the runtime never needs to evaluate inside a vector.
25296 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25297 /// has already consumed the `EXTRACT` token before calling us —
25298 /// we pick up at the opening `(`.
25299 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25300 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25301 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25302 /// per-column OR-fold of
25303 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25304 /// term)` so the existing FTS evaluator handles semantics.
25305 ///
25306 /// The mode modifier is accepted-and-ignored at v7.17 — all
25307 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25308 /// mode operators (`+foo -bar`) would need their own parser
25309 /// (Phase 2.2c); customers who hit them today already get a
25310 /// correct lexeme-match against the bare term, only without
25311 /// the +/- precedence the customer asked for.
25312 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25313 // Already at `MATCH`-consumed position; the dispatcher
25314 // confirmed the next token is `(`.
25315 if !matches!(self.peek(), Token::LParen) {
25316 return Err(self.err(alloc::format!(
25317 "expected '(' after MATCH, got {:?}",
25318 self.peek()
25319 )));
25320 }
25321 self.advance();
25322 let mut cols: Vec<Expr> = Vec::new();
25323 loop {
25324 cols.push(self.parse_expr(0)?);
25325 match self.peek() {
25326 Token::Comma => {
25327 self.advance();
25328 }
25329 Token::RParen => break,
25330 other => {
25331 return Err(self.err(alloc::format!(
25332 "expected ',' or ')' in MATCH column list, got {other:?}"
25333 )));
25334 }
25335 }
25336 }
25337 self.advance(); // ')'
25338 // Expect AGAINST.
25339 match self.peek() {
25340 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25341 self.advance();
25342 }
25343 other => {
25344 return Err(self.err(alloc::format!(
25345 "expected AGAINST after MATCH column list, got {other:?}"
25346 )));
25347 }
25348 }
25349 if !matches!(self.peek(), Token::LParen) {
25350 return Err(self.err(alloc::format!(
25351 "expected '(' after AGAINST, got {:?}",
25352 self.peek()
25353 )));
25354 }
25355 self.advance();
25356 // Read AGAINST's argument as a single primary token —
25357 // string literal, placeholder, or column-ref ident. We
25358 // can't call `parse_expr` / `parse_unary` here because
25359 // the postfix chain inside `parse_atom` would greedily
25360 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25361 // and fail at "expected '(' after IN". Customers always
25362 // write a literal or bound parameter in AGAINST, so this
25363 // restriction is non-blocking; the error path explains
25364 // the limit if a more complex expression shows up.
25365 let term = match self.advance() {
25366 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25367 Token::Placeholder(n) => Expr::Placeholder(n),
25368 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25369 qualifier: None,
25370 name: s,
25371 }),
25372 other => {
25373 return Err(self.err(alloc::format!(
25374 "MATCH ... AGAINST(<term>) expects a string literal, \
25375 bound parameter, or column ref, got {other:?}"
25376 )));
25377 }
25378 };
25379 // Optional mode tail — accept-and-ignore at v7.17:
25380 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25381 // IN BOOLEAN MODE
25382 // WITH QUERY EXPANSION
25383 loop {
25384 match self.peek() {
25385 // IN lexes as a reserved Token::In, not an ident,
25386 // so it gets its own arm.
25387 Token::In => {
25388 self.advance();
25389 }
25390 Token::Ident(s) | Token::QuotedIdent(s)
25391 if s.eq_ignore_ascii_case("natural")
25392 || s.eq_ignore_ascii_case("language")
25393 || s.eq_ignore_ascii_case("boolean")
25394 || s.eq_ignore_ascii_case("mode")
25395 || s.eq_ignore_ascii_case("with")
25396 || s.eq_ignore_ascii_case("query")
25397 || s.eq_ignore_ascii_case("expansion") =>
25398 {
25399 self.advance();
25400 }
25401 _ => break,
25402 }
25403 }
25404 if !matches!(self.peek(), Token::RParen) {
25405 return Err(self.err(alloc::format!(
25406 "expected ')' to close AGAINST, got {:?}",
25407 self.peek()
25408 )));
25409 }
25410 self.advance();
25411 // Build per-column `to_tsvector('simple', col) @@
25412 // plainto_tsquery('simple', term)` and OR-fold.
25413 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25414 let plainto = Expr::FunctionCall {
25415 name: String::from("plainto_tsquery"),
25416 args: alloc::vec![simple_lit(), term.clone()],
25417 };
25418 let mut folded: Option<Expr> = None;
25419 for col in cols {
25420 let to_tsv = Expr::FunctionCall {
25421 name: String::from("to_tsvector"),
25422 args: alloc::vec![simple_lit(), col],
25423 };
25424 let leaf = Expr::Binary {
25425 lhs: Box::new(to_tsv),
25426 op: crate::ast::BinOp::TsMatch,
25427 rhs: Box::new(plainto.clone()),
25428 };
25429 folded = Some(match folded {
25430 None => leaf,
25431 Some(prev) => Expr::Binary {
25432 lhs: Box::new(prev),
25433 op: crate::ast::BinOp::Or,
25434 rhs: Box::new(leaf),
25435 },
25436 });
25437 }
25438 match folded {
25439 Some(e) => Ok(e),
25440 None => Err(self.err(String::from(
25441 "MATCH(...) AGAINST(...) requires at least one column",
25442 ))),
25443 }
25444 }
25445
25446 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25447 if !matches!(self.peek(), Token::LParen) {
25448 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25449 }
25450 self.advance();
25451 let field_name = self.expect_ident_like()?;
25452 let field = match field_name.to_ascii_lowercase().as_str() {
25453 // PG accepts the plural spellings (years/months/…/millenniums) as
25454 // aliases for the singular fields — its datetime unit table has both.
25455 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25456 "year" | "years" => ExtractField::Year,
25457 "month" | "months" => ExtractField::Month,
25458 "day" | "days" => ExtractField::Day,
25459 "hour" | "hours" => ExtractField::Hour,
25460 "minute" | "minutes" => ExtractField::Minute,
25461 "second" | "seconds" => ExtractField::Second,
25462 "microsecond" | "microseconds" => ExtractField::Microsecond,
25463 "epoch" => ExtractField::Epoch,
25464 "dow" => ExtractField::Dow,
25465 "isodow" => ExtractField::Isodow,
25466 "doy" => ExtractField::Doy,
25467 "week" | "weeks" => ExtractField::Week,
25468 "isoyear" => ExtractField::Isoyear,
25469 "quarter" => ExtractField::Quarter,
25470 "decade" | "decades" => ExtractField::Decade,
25471 "century" | "centuries" => ExtractField::Century,
25472 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25473 "julian" => ExtractField::Julian,
25474 "millisecond" | "milliseconds" => ExtractField::Millisecond,
25475 "timezone" => ExtractField::Timezone,
25476 "timezone_hour" => ExtractField::TimezoneHour,
25477 "timezone_minute" => ExtractField::TimezoneMinute,
25478 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25479 // reports an unknown one with the source type (22023); carry the
25480 // raw name so eval can word it.
25481 other => ExtractField::Other(alloc::string::String::from(other)),
25482 };
25483 if !matches!(self.peek(), Token::From) {
25484 return Err(self.err(format!(
25485 "expected FROM after EXTRACT field, got {:?}",
25486 self.peek()
25487 )));
25488 }
25489 self.advance();
25490 let source = self.parse_expr(0)?;
25491 if !matches!(self.peek(), Token::RParen) {
25492 return Err(self.err(format!(
25493 "expected ')' to close EXTRACT, got {:?}",
25494 self.peek()
25495 )));
25496 }
25497 self.advance();
25498 Ok(Expr::Extract {
25499 field,
25500 source: Box::new(source),
25501 })
25502 }
25503
25504 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25505 /// is already consumed; we expect a single string literal next and
25506 /// resolve it into `Literal::Interval` at parse time so the engine
25507 /// never has to re-tokenise inside the string.
25508 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25509 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25510 /// is the SQL-standard form and is left to the path below.
25511 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25512 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25513 let (offset, sign) = match self.peek() {
25514 Token::Minus => (1, "-"),
25515 _ => (0, ""),
25516 };
25517 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25518 return None;
25519 };
25520 self.tokens
25521 .get(self.pos + offset + 1)
25522 .filter(|t| mysql_interval_unit(t).is_some())?;
25523 Some((alloc::format!("{sign}{n}"), offset + 1))
25524 }
25525
25526 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25527 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25528 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25529 ///
25530 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25531 /// this by parsing the group and then restoring `self.pos` — which could
25532 /// never have worked, because `advance()` DESTROYS the token it returns
25533 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25534 /// inert only because both branches errored back then.
25535 fn interval_paren_is_quantity(&self) -> bool {
25536 let mut depth = 0usize;
25537 let mut saw_top_level_comma = false;
25538 let mut i = self.pos;
25539 while let Some(tok) = self.tokens.get(i) {
25540 match tok {
25541 Token::LParen => depth += 1,
25542 Token::RParen => {
25543 depth = depth.saturating_sub(1);
25544 if depth == 0 {
25545 return !saw_top_level_comma
25546 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25547 .is_some();
25548 }
25549 }
25550 // A comma directly inside the outermost parens means the
25551 // argument list of the INTERVAL() function.
25552 Token::Comma if depth == 1 => saw_top_level_comma = true,
25553 Token::Eof => return false,
25554 _ => {}
25555 }
25556 i += 1;
25557 }
25558 false
25559 }
25560
25561 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25562 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25563 // (the index of the last Ni ≤ N), distinct from the interval literal.
25564 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25565 // is decided by a non-destructive lookahead (round 422) before either
25566 // branch consumes anything. MySQL only.
25567 if self.mysql_dialect
25568 && matches!(self.peek(), Token::LParen)
25569 && !self.interval_paren_is_quantity()
25570 {
25571 self.advance(); // (
25572 let mut args = Vec::new();
25573 if !matches!(self.peek(), Token::RParen) {
25574 loop {
25575 args.push(self.parse_expr(0)?);
25576 if matches!(self.peek(), Token::Comma) {
25577 self.advance();
25578 continue;
25579 }
25580 break;
25581 }
25582 }
25583 if !matches!(self.peek(), Token::RParen) {
25584 return Err(self.err(alloc::format!(
25585 "expected ')' after INTERVAL() arguments, got {:?}",
25586 self.peek()
25587 )));
25588 }
25589 self.advance(); // )
25590 return Ok(Expr::FunctionCall {
25591 name: alloc::string::String::from("interval"),
25592 args,
25593 });
25594 }
25595 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25596 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25597 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25598 // writes every date arithmetic there is, and it did not parse at
25599 // all. PG rejects the unquoted form outright (`syntax error at or
25600 // near "1"`, measured), so it is taken only in the MySQL dialect —
25601 // PG's own `INTERVAL '1' DAY` is untouched below.
25602 if self.mysql_dialect
25603 && let Some((text, consume)) = self.peek_unquoted_interval_count()
25604 {
25605 for _ in 0..consume {
25606 self.advance(); // the optional `-` and the number
25607 }
25608 let Some(unit) = mysql_interval_unit(self.peek()) else {
25609 return Err(self.err(alloc::format!(
25610 "expected an interval unit after INTERVAL {text}, got {:?}",
25611 self.peek()
25612 )));
25613 };
25614 self.advance(); // the unit
25615 let (months, days, micros) = scale_mysql_interval(&text, unit)
25616 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25617 return Ok(Expr::Literal(Literal::Interval {
25618 months,
25619 days,
25620 micros,
25621 // The canonical rendering, so Display round-trips into a
25622 // form both dialects read back.
25623 text: alloc::format!("{text} {unit}"),
25624 }));
25625 }
25626 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25627 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25628 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25629 // Those cannot fold into a compile-time `Literal::Interval`, so they
25630 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25631 // builtin, which builds the value at run time (and yields NULL for a
25632 // NULL quantity, as MariaDB does). The literal path above still folds
25633 // the constant case — it is cheaper and round-trips through Display.
25634 //
25635 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25636 // MySQL's quoted spelling) keep the qualifier path below.
25637 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25638 let qty = self.parse_expr(0)?;
25639 let Some(unit) = mysql_interval_unit(self.peek()) else {
25640 return Err(self.err(alloc::format!(
25641 "expected an interval unit after INTERVAL <expr>, got {:?}",
25642 self.peek()
25643 )));
25644 };
25645 self.advance(); // the unit
25646 return Ok(make_interval_call(qty, unit));
25647 }
25648 let tok = self.advance();
25649 let Token::String(text) = tok else {
25650 return Err(self.err(format!(
25651 "expected string literal after INTERVAL, got {tok:?}"
25652 )));
25653 };
25654 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25655 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25656 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25657 // bare number means and the leading/trailing precision.
25658 let field1 = interval_field_of(self.peek());
25659 let qualifier = if let Some(f1) = field1 {
25660 self.advance();
25661 let f2 = if matches!(self.peek(), Token::To) {
25662 self.advance();
25663 let Some(f) = interval_field_of(self.peek()) else {
25664 return Err(self.err(format!(
25665 "expected an interval field after TO, got {:?}",
25666 self.peek()
25667 )));
25668 };
25669 self.advance();
25670 Some(f)
25671 } else {
25672 None
25673 };
25674 Some((f1, f2))
25675 } else {
25676 None
25677 };
25678 let (months, days, micros) = match qualifier {
25679 Some(q) => interpret_qualified_interval(&text, q),
25680 None => parse_interval_text(&text),
25681 }
25682 .ok_or_else(|| ParseError {
25683 message: format!(
25684 "cannot parse INTERVAL {text:?}; \
25685 expected `<n> <unit> [<n> <unit> ...]` with units \
25686 microsecond[s], millisecond[s], second[s], minute[s], \
25687 hour[s], day[s], week[s], month[s], year[s]"
25688 ),
25689 token_pos: self.consumed_pos(),
25690 })?;
25691 Ok(Expr::Literal(Literal::Interval {
25692 months,
25693 days,
25694 micros,
25695 text,
25696 }))
25697 }
25698
25699 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25700 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25701 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25702 /// than a pgvector literal.
25703 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25704 self.advance(); // consume `[`
25705 let mut items: Vec<Expr> = Vec::new();
25706 if !matches!(self.peek(), Token::RBracket) {
25707 loop {
25708 if matches!(self.peek(), Token::LBracket) {
25709 items.push(self.parse_array_bracket_body()?);
25710 } else {
25711 items.push(self.parse_expr(0)?);
25712 }
25713 match self.peek() {
25714 Token::Comma => {
25715 self.advance();
25716 }
25717 Token::RBracket => break,
25718 other => {
25719 return Err(self.err(alloc::format!(
25720 "expected ',' or ']' in array literal, got {other:?}"
25721 )));
25722 }
25723 }
25724 }
25725 }
25726 self.advance(); // consume `]`
25727 Ok(Expr::Array(items))
25728 }
25729
25730 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25731 let mut elems = Vec::new();
25732 if matches!(self.peek(), Token::RBracket) {
25733 self.advance();
25734 return Ok(Expr::Literal(Literal::Vector(elems)));
25735 }
25736 loop {
25737 let e = self.parse_expr(0)?;
25738 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25739 message: format!("vector element must be a numeric literal, got {e:?}"),
25740 token_pos: self.pos,
25741 })?;
25742 elems.push(x);
25743 match self.peek() {
25744 Token::Comma => {
25745 self.advance();
25746 }
25747 Token::RBracket => {
25748 self.advance();
25749 break;
25750 }
25751 other => {
25752 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25753 }
25754 }
25755 }
25756 Ok(Expr::Literal(Literal::Vector(elems)))
25757 }
25758
25759 /// Atom that started with an identifier: could be `t.col`, `col`, or
25760 /// `func(arg, ...)`. Detect each shape by looking at the next token.
25761 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25762 /// [, ...])`. Caller has already consumed `OVER`. Either clause
25763 /// is optional; an empty `()` is also legal (PG semantics).
25764 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25765 /// modifier between `name(args)` and `OVER (...)`. Default is
25766 /// `Respect`. Unrecognised idents leave the stream unchanged.
25767 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25768 let Token::Ident(s) = self.peek().clone() else {
25769 return NullTreatment::Respect;
25770 };
25771 let is_ignore = s.eq_ignore_ascii_case("ignore");
25772 let is_respect = s.eq_ignore_ascii_case("respect");
25773 if !is_ignore && !is_respect {
25774 return NullTreatment::Respect;
25775 }
25776 // Lookahead for NULLS — only consume both tokens together.
25777 // pos+1 must hold a "nulls" ident.
25778 if self.pos + 1 < self.tokens.len()
25779 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25780 && s2.eq_ignore_ascii_case("nulls")
25781 {
25782 self.advance();
25783 self.advance();
25784 return if is_ignore {
25785 NullTreatment::Ignore
25786 } else {
25787 NullTreatment::Respect
25788 };
25789 }
25790 NullTreatment::Respect
25791 }
25792
25793 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25794 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25795 /// (same shape as the `OVER` tail). Consumes the whole clause and
25796 /// returns the predicate; returns `None` when no `FILTER` follows.
25797 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25798 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25799 return Ok(None);
25800 };
25801 if !s.eq_ignore_ascii_case("filter") {
25802 return Ok(None);
25803 }
25804 self.advance(); // FILTER
25805 if !matches!(self.peek(), Token::LParen) {
25806 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25807 }
25808 self.advance(); // (
25809 if !matches!(self.peek(), Token::Where) {
25810 return Err(self.err(format!(
25811 "expected WHERE inside FILTER (...), got {:?}",
25812 self.peek()
25813 )));
25814 }
25815 self.advance(); // WHERE
25816 let cond = self.parse_expr(0)?;
25817 if !matches!(self.peek(), Token::RParen) {
25818 return Err(self.err(format!(
25819 "expected ')' to close FILTER (WHERE ...), got {:?}",
25820 self.peek()
25821 )));
25822 }
25823 self.advance(); // )
25824 Ok(Some(Box::new(cond)))
25825 }
25826
25827 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25828 /// the separator as the aggregate's second argument, which is the
25829 /// shape `string_agg` already takes. Returns whether one was there.
25830 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25831 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25832 return Ok(false);
25833 }
25834 self.advance();
25835 let Token::String(sep) = self.peek().clone() else {
25836 return Err(self.err(alloc::format!(
25837 "expected a string literal after SEPARATOR, got {:?}",
25838 self.peek()
25839 )));
25840 };
25841 self.advance();
25842 args.push(Expr::Literal(Literal::String(sep)));
25843 Ok(true)
25844 }
25845
25846 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25847 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25848 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25849 /// keys, or an empty vec when no `WITHIN GROUP` follows.
25850 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25851 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25852 return Ok(Vec::new());
25853 };
25854 if !s.eq_ignore_ascii_case("within") {
25855 return Ok(Vec::new());
25856 }
25857 self.advance(); // WITHIN
25858 if !matches!(self.peek(), Token::Group) {
25859 return Err(self.err(format!(
25860 "expected GROUP after WITHIN, got {:?}",
25861 self.peek()
25862 )));
25863 }
25864 self.advance(); // GROUP
25865 if !matches!(self.peek(), Token::LParen) {
25866 return Err(self.err(format!(
25867 "expected '(' after WITHIN GROUP, got {:?}",
25868 self.peek()
25869 )));
25870 }
25871 self.advance(); // (
25872 if !matches!(self.peek(), Token::Order) {
25873 return Err(self.err(format!(
25874 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25875 self.peek()
25876 )));
25877 }
25878 self.advance(); // ORDER
25879 if !self.peek_is_by() {
25880 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25881 }
25882 self.advance(); // BY
25883 let mut keys: Vec<OrderBy> = Vec::new();
25884 loop {
25885 // v7.39 (round 691) — save/restore, the discipline this parser
25886 // already uses around `pending_sample_preds`, so a subquery inside
25887 // a key neither inherits nor leaks the channel.
25888 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25889 let saved_coll = self.order_key_collation.take();
25890 let parsed = self.parse_expr(0);
25891 self.in_order_by_key = saved_flag;
25892 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25893 let expr = parsed?;
25894 let desc = if matches!(self.peek(), Token::Desc) {
25895 self.advance();
25896 true
25897 } else if matches!(self.peek(), Token::Asc) {
25898 self.advance();
25899 false
25900 } else {
25901 false
25902 };
25903 let nulls_first = self.parse_optional_nulls_placement()?;
25904 keys.push(OrderBy {
25905 expr,
25906 desc,
25907 nulls_first,
25908 collation,
25909 });
25910 if matches!(self.peek(), Token::Comma) {
25911 self.advance();
25912 } else {
25913 break;
25914 }
25915 }
25916 if !matches!(self.peek(), Token::RParen) {
25917 return Err(self.err(format!(
25918 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25919 self.peek()
25920 )));
25921 }
25922 self.advance(); // )
25923 Ok(keys)
25924 }
25925
25926 /// No frame clause is supported.
25927 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25928 fn parse_over_clause(
25929 &mut self,
25930 ) -> Result<
25931 (
25932 Vec<Expr>,
25933 Vec<(Expr, bool, Option<bool>)>,
25934 Option<WindowFrame>,
25935 ),
25936 ParseError,
25937 > {
25938 // `OVER w` — a named-window reference. The WINDOW clause
25939 // parses after the select list, so the name rides out as a
25940 // marker in partition_by; parse_bare_select substitutes the
25941 // definition once the clause is known.
25942 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25943 let name = w.clone();
25944 self.advance();
25945 return Ok((
25946 alloc::vec![Expr::Column(crate::ast::ColumnName {
25947 qualifier: Some("__named_window__".to_string()),
25948 name,
25949 })],
25950 Vec::new(),
25951 None,
25952 ));
25953 }
25954 if !matches!(self.peek(), Token::LParen) {
25955 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25956 }
25957 self.advance();
25958 let mut partition_by = Vec::new();
25959 let mut order_by = Vec::new();
25960 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25961 // window, refined in place. PG's rules (probed against 18.4) differ
25962 // from the bare `OVER w1` form, so the reference rides out under its
25963 // own marker and `substitute_named_windows` applies them. The base
25964 // name is any leading identifier that isn't a window-spec keyword.
25965 let base_window = match self.peek() {
25966 Token::Ident(s) | Token::QuotedIdent(s)
25967 if !s.eq_ignore_ascii_case("partition")
25968 && !s.eq_ignore_ascii_case("rows")
25969 && !s.eq_ignore_ascii_case("range")
25970 && !s.eq_ignore_ascii_case("groups") =>
25971 {
25972 let n = s.clone();
25973 self.advance();
25974 Some(n)
25975 }
25976 _ => None,
25977 };
25978 // PARTITION BY ?
25979 // v7.37.6-B promoted PARTITION to a reserved keyword
25980 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25981 // `Token::Ident("partition")`. Accept both so older sources
25982 // and the new lexer surface land on the same path.
25983 let is_partition_kw = match self.peek() {
25984 Token::Partition => true,
25985 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25986 _ => false,
25987 };
25988 if is_partition_kw {
25989 self.advance();
25990 if !self.peek_is_by() {
25991 return Err(self.err(format!(
25992 "expected BY after PARTITION, got {:?}",
25993 self.peek()
25994 )));
25995 }
25996 self.advance();
25997 loop {
25998 partition_by.push(self.parse_expr(0)?);
25999 if matches!(self.peek(), Token::Comma) {
26000 self.advance();
26001 continue;
26002 }
26003 break;
26004 }
26005 }
26006 // ORDER BY ?
26007 if matches!(self.peek(), Token::Order) {
26008 self.advance();
26009 if !self.peek_is_by() {
26010 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
26011 }
26012 self.advance();
26013 loop {
26014 let e = self.parse_expr(0)?;
26015 let desc = if matches!(self.peek(), Token::Desc) {
26016 self.advance();
26017 true
26018 } else if matches!(self.peek(), Token::Asc) {
26019 self.advance();
26020 false
26021 } else {
26022 false
26023 };
26024 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
26025 let nulls_first = self.parse_optional_nulls_placement()?;
26026 order_by.push((e, desc, nulls_first));
26027 if matches!(self.peek(), Token::Comma) {
26028 self.advance();
26029 continue;
26030 }
26031 break;
26032 }
26033 }
26034 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
26035 // Both keywords come through the lexer as identifiers; match
26036 // case-insensitively.
26037 let mut frame: Option<WindowFrame> = None;
26038 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
26039 let kind = if s.eq_ignore_ascii_case("rows") {
26040 Some(FrameKind::Rows)
26041 } else if s.eq_ignore_ascii_case("range") {
26042 Some(FrameKind::Range)
26043 } else if s.eq_ignore_ascii_case("groups") {
26044 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
26045 Some(FrameKind::Groups)
26046 } else {
26047 None
26048 };
26049 if let Some(kind) = kind {
26050 self.advance();
26051 frame = Some(self.parse_frame_tail(kind)?);
26052 }
26053 }
26054 if !matches!(self.peek(), Token::RParen) {
26055 return Err(self.err(format!(
26056 "expected ')' to close OVER clause, got {:?}",
26057 self.peek()
26058 )));
26059 }
26060 self.advance();
26061 if let Some(base) = base_window {
26062 // A copy may refine but never override the base's partitioning
26063 // (PG rejects it outright, before looking the name up).
26064 if !partition_by.is_empty() {
26065 return Err(self.err(alloc::format!(
26066 "cannot override PARTITION BY clause of window \"{base}\""
26067 )));
26068 }
26069 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
26070 qualifier: Some("__named_window_ref__".to_string()),
26071 name: base,
26072 })];
26073 }
26074 Ok((partition_by, order_by, frame))
26075 }
26076
26077 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
26078 /// or `RANGE` keyword was just consumed. Accepts both
26079 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
26080 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
26081 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
26082 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
26083 let (start, end) = if matches!(self.peek(), Token::Between) {
26084 self.advance();
26085 let start = self.parse_frame_bound()?;
26086 if !matches!(self.peek(), Token::And) {
26087 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
26088 }
26089 self.advance();
26090 let end = self.parse_frame_bound()?;
26091 (start, Some(end))
26092 } else {
26093 (self.parse_frame_bound()?, None)
26094 };
26095 let exclude = self.parse_frame_exclusion()?;
26096 Ok(WindowFrame {
26097 kind,
26098 start,
26099 end,
26100 exclude,
26101 })
26102 }
26103
26104 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
26105 /// after a frame spec. NO OTHERS is the default no-op.
26106 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
26107 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
26108 return Ok(FrameExclusion::NoOthers);
26109 }
26110 self.advance(); // EXCLUDE
26111 match self.peek() {
26112 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
26113 self.advance();
26114 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
26115 return Err(self.err(format!(
26116 "expected ROW after EXCLUDE CURRENT, got {:?}",
26117 self.peek()
26118 )));
26119 }
26120 self.advance();
26121 Ok(FrameExclusion::CurrentRow)
26122 }
26123 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
26124 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
26125 // Without this arm it fell to the catch-all, whose message
26126 // self-contradictingly listed GROUP as expected.
26127 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
26128 self.advance();
26129 Ok(FrameExclusion::Group)
26130 }
26131 Token::Group => {
26132 self.advance();
26133 Ok(FrameExclusion::Group)
26134 }
26135 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
26136 self.advance();
26137 Ok(FrameExclusion::Ties)
26138 }
26139 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
26140 self.advance();
26141 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
26142 return Err(self.err(format!(
26143 "expected OTHERS after EXCLUDE NO, got {:?}",
26144 self.peek()
26145 )));
26146 }
26147 self.advance();
26148 Ok(FrameExclusion::NoOthers)
26149 }
26150 other => Err(self.err(format!(
26151 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
26152 ))),
26153 }
26154 }
26155
26156 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
26157 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
26158 /// `UNBOUNDED FOLLOWING`.
26159 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26160 // Interval-typed offset for a value-based RANGE frame over a
26161 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26162 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26163 // PRECEDING`.
26164 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26165 let dir = self.expect_ident_like()?;
26166 return if dir.eq_ignore_ascii_case("preceding") {
26167 Ok(FrameBound::IntervalPreceding {
26168 months,
26169 days,
26170 micros,
26171 })
26172 } else if dir.eq_ignore_ascii_case("following") {
26173 Ok(FrameBound::IntervalFollowing {
26174 months,
26175 days,
26176 micros,
26177 })
26178 } else {
26179 Err(self.err(format!(
26180 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26181 )))
26182 };
26183 }
26184 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26185 if let Token::Integer(n) = *self.peek() {
26186 self.advance();
26187 let n: u64 = u64::try_from(n).map_err(|_| {
26188 self.err(format!(
26189 "invalid frame offset {n} — expected non-negative integer"
26190 ))
26191 })?;
26192 let dir = self.expect_ident_like()?;
26193 return if dir.eq_ignore_ascii_case("preceding") {
26194 Ok(FrameBound::OffsetPreceding(n))
26195 } else if dir.eq_ignore_ascii_case("following") {
26196 Ok(FrameBound::OffsetFollowing(n))
26197 } else {
26198 Err(self.err(format!(
26199 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26200 )))
26201 };
26202 }
26203 let first = self.expect_ident_like()?;
26204 if first.eq_ignore_ascii_case("unbounded") {
26205 let dir = self.expect_ident_like()?;
26206 return if dir.eq_ignore_ascii_case("preceding") {
26207 Ok(FrameBound::UnboundedPreceding)
26208 } else if dir.eq_ignore_ascii_case("following") {
26209 Ok(FrameBound::UnboundedFollowing)
26210 } else {
26211 Err(self.err(format!(
26212 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26213 )))
26214 };
26215 }
26216 if first.eq_ignore_ascii_case("current") {
26217 let row = self.expect_ident_like()?;
26218 if !row.eq_ignore_ascii_case("row") {
26219 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26220 }
26221 return Ok(FrameBound::CurrentRow);
26222 }
26223 Err(self.err(format!(
26224 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26225 )))
26226 }
26227
26228 /// Detect and consume a leading interval offset in a frame bound —
26229 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26230 /// `(months, days, micros)`. Leaves the cursor on the trailing
26231 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26232 /// when the next tokens are not an interval offset.
26233 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26234 // Shape A — `INTERVAL '1 day'`.
26235 if matches!(self.peek(), Token::Interval) {
26236 self.advance(); // INTERVAL
26237 let atom = self.parse_interval_atom()?;
26238 if let Expr::Literal(Literal::Interval {
26239 months,
26240 days,
26241 micros,
26242 ..
26243 }) = atom
26244 {
26245 return Ok(Some((months, days, micros)));
26246 }
26247 return Err(self.err("expected an interval literal in frame offset".to_string()));
26248 }
26249 // Shape B — `'1 day'::interval`. Look ahead for the exact
26250 // string / `::` / interval-target triple before committing.
26251 if let Token::String(text) = self.peek() {
26252 let target_is_interval = match self.tokens.get(self.pos + 2) {
26253 Some(Token::Interval) => true,
26254 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26255 _ => false,
26256 };
26257 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26258 && target_is_interval;
26259 if is_cast {
26260 let text = text.clone();
26261 self.advance(); // string
26262 self.advance(); // ::
26263 self.advance(); // interval
26264 let parts = parse_interval_text(&text).ok_or_else(|| {
26265 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26266 })?;
26267 return Ok(Some(parts));
26268 }
26269 }
26270 Ok(None)
26271 }
26272
26273 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26274 // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26275 // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26276 // and all three answer the literal on MySQL 9.7.2.
26277 //
26278 // It is not only syntax, which is why it waited for
26279 // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26280 // because `_binary` makes the comparison byte-wise, while
26281 // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26282 // dropping the charset would have turned a hard error into a
26283 // silently wrong comparison — worse than the error it replaced.
26284 //
26285 // An UNKNOWN charset is NOT an introducer: MySQL answers
26286 // `Unknown column '_nosuch'`, because it parses as a column
26287 // reference followed by a string. So the table decides, and it
26288 // is the same table `SET NAMES` reads.
26289 //
26290 // A space is allowed between the two (`_utf8mb4 'x'`), which
26291 // falls out of asking the token stream rather than the bytes.
26292 if self.mysql_dialect
26293 && let Token::String(_) = self.peek()
26294 {
26295 let lower = first.to_ascii_lowercase();
26296 let charset = if lower == "n" {
26297 // `N'…'` is the national character set, which MySQL
26298 // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26299 //
26300 // utf8mb3 and utf8mb4 both fold case in their default
26301 // collations, so nothing SPG can be asked distinguishes
26302 // the two here: an ablation swapping this to utf8mb4
26303 // reddens no pin. Recorded rather than implied — the
26304 // spelling follows MySQL's documentation, not a
26305 // measurement.
26306 Some("utf8mb3")
26307 } else {
26308 // No filter here: the lookup below IS the test for
26309 // "is this a charset". An ablation that removed a filter
26310 // in this spot reddened nothing, which is how the two
26311 // were found to be one check written twice.
26312 lower.strip_prefix('_')
26313 };
26314 if let Some(cs) = charset
26315 && let Some(collation) = crate::charset::charset_default_collation(cs)
26316 {
26317 let Token::String(body) = self.advance() else {
26318 unreachable!("peeked a string");
26319 };
26320 return Ok(Expr::Collate {
26321 expr: Box::new(Expr::Literal(Literal::String(body))),
26322 collation: String::from(collation),
26323 });
26324 }
26325 }
26326 if matches!(self.peek(), Token::Dot) {
26327 self.advance();
26328 let name = self.expect_ident_like()?;
26329 // v7.14.0 — schema-qualified function call
26330 // `<schema>.<fn>(args)`. PG dumps emit
26331 // `pg_catalog.set_config(...)` in the preamble. SPG
26332 // is single-namespace: drop the schema prefix and
26333 // route the dispatch on the bare function name.
26334 if matches!(self.peek(), Token::LParen) {
26335 return self.finish_ident_atom(name);
26336 }
26337 return Ok(Expr::Column(ColumnName {
26338 qualifier: Some(first),
26339 name,
26340 }));
26341 }
26342 if matches!(self.peek(), Token::LParen) {
26343 self.advance();
26344 // `COUNT(*)` — special-cased here because `*` isn't a normal
26345 // expression token. Lower-case match on `first` since the lexer
26346 // folds identifiers.
26347 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26348 self.advance();
26349 if !matches!(self.peek(), Token::RParen) {
26350 return Err(self.err(format!(
26351 "expected ')' after COUNT(*), got {:?}",
26352 self.peek()
26353 )));
26354 }
26355 self.advance();
26356 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26357 let filter = self.parse_filter_clause()?;
26358 // v4.12: COUNT(*) OVER (...) — same window tail.
26359 let null_treatment = self.parse_null_treatment_modifier();
26360 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26361 && s.eq_ignore_ascii_case("over")
26362 {
26363 self.advance();
26364 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26365 return Ok(Expr::WindowFunction {
26366 name: "count_star".into(),
26367 args: Vec::new(),
26368 partition_by,
26369 order_by,
26370 frame,
26371 null_treatment,
26372 filter,
26373 });
26374 }
26375 if let Some(filter) = filter {
26376 return Ok(Expr::AggregateOrdered {
26377 call: Box::new(Expr::FunctionCall {
26378 name: "count_star".into(),
26379 args: Vec::new(),
26380 }),
26381 order_by: Vec::new(),
26382 distinct: false,
26383 filter: Some(filter),
26384 });
26385 }
26386 return Ok(Expr::FunctionCall {
26387 name: "count_star".into(),
26388 args: Vec::new(),
26389 });
26390 }
26391 // Function call. PG-style: zero-or-more comma-separated args.
26392 let mut args = Vec::new();
26393 // v7.38 (read01, T14) — named-argument notation `argname => value`.
26394 // Names are collected in lock-step with `args` and resolved to
26395 // positional order after the loop (the AST stays positional).
26396 let mut arg_names: Vec<Option<String>> = Vec::new();
26397 let mut agg_order_by: Vec<OrderBy> = Vec::new();
26398 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26399 // seen, so the value arguments before it can be folded.
26400 let mut saw_separator = false;
26401 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26402 // v7.32 (round-29) — accept the dual `ALL` quantifier too
26403 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26404 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26405 self.advance();
26406 true
26407 } else if matches!(self.peek(), Token::All) {
26408 self.advance();
26409 false
26410 } else {
26411 false
26412 };
26413 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26414 // TIMESTAMPDIFF take a bare unit keyword as the first
26415 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26416 // bare type keyword (DATE / TIME / DATETIME); lower them
26417 // onto string literals so the evaluator sees plain text.
26418 if ((first.eq_ignore_ascii_case("timestampadd")
26419 || first.eq_ignore_ascii_case("timestampdiff"))
26420 && matches!(self.peek(), Token::Ident(u) if matches!(
26421 u.to_ascii_lowercase().as_str(),
26422 "microsecond" | "second" | "minute" | "hour" | "day"
26423 | "week" | "month" | "quarter" | "year"
26424 )))
26425 || (first.eq_ignore_ascii_case("get_format")
26426 && matches!(self.peek(), Token::Ident(u) if matches!(
26427 u.to_ascii_lowercase().as_str(),
26428 "date" | "time" | "datetime" | "timestamp"
26429 )))
26430 {
26431 if let Token::Ident(u) = self.peek() {
26432 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26433 }
26434 self.advance();
26435 if matches!(self.peek(), Token::Comma) {
26436 self.advance();
26437 }
26438 }
26439 // `ROW(a, b, …)` keyword constructor. Followed by a
26440 // comparison operator or [NOT] IN it joins the paren
26441 // row-constructor machinery (fieldwise parse-time
26442 // expansion); bare, it stays a `row` call the evaluator
26443 // renders as PG record text.
26444 if first.eq_ignore_ascii_case("row") {
26445 let mut row_items = Vec::new();
26446 if !matches!(self.peek(), Token::RParen) {
26447 loop {
26448 row_items.push(self.parse_expr(0)?);
26449 match self.peek() {
26450 Token::Comma => {
26451 self.advance();
26452 }
26453 Token::RParen => break,
26454 other => {
26455 return Err(self.err(format!(
26456 "expected ',' or ')' in ROW(...), got {other:?}"
26457 )));
26458 }
26459 }
26460 }
26461 }
26462 self.advance(); // ')'
26463 let comparison_follows = matches!(
26464 self.peek(),
26465 Token::Eq
26466 | Token::NotEq
26467 | Token::Lt
26468 | Token::LtEq
26469 | Token::Gt
26470 | Token::GtEq
26471 | Token::In
26472 ) || (matches!(self.peek(), Token::Not)
26473 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26474 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26475 if comparison_follows && !row_items.is_empty() {
26476 return self.parse_row_comparison_tail(row_items);
26477 }
26478 return Ok(Expr::FunctionCall {
26479 name: String::from("row"),
26480 args: row_items,
26481 });
26482 }
26483 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26484 // the parse-mode keyword introduces the source text. SPG
26485 // carries XML as text, so both modes lower to __xmlparse(expr)
26486 // which validates well-formedness and returns Value::Xml.
26487 if first.eq_ignore_ascii_case("xmlparse")
26488 && matches!(self.peek(), Token::Ident(kw)
26489 if kw.eq_ignore_ascii_case("document")
26490 || kw.eq_ignore_ascii_case("content"))
26491 {
26492 let mode = match self.advance() {
26493 Token::Ident(kw) => kw.to_ascii_lowercase(),
26494 _ => unreachable!("peeked an ident"),
26495 };
26496 let src = self.parse_expr(0)?;
26497 if !matches!(self.peek(), Token::RParen) {
26498 return Err(self.err(format!(
26499 "expected ')' to close XMLPARSE, got {:?}",
26500 self.peek()
26501 )));
26502 }
26503 self.advance();
26504 return Ok(Expr::FunctionCall {
26505 name: String::from("__xmlparse"),
26506 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26507 });
26508 }
26509 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26510 // keyword introduces the element name (a bare or quoted
26511 // identifier), then optional content expressions. Lower to a
26512 // plain `xmlelement(name_text, content …)` call.
26513 if first.eq_ignore_ascii_case("xmlelement")
26514 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26515 {
26516 self.advance(); // consume NAME
26517 let elem_name = match self.peek().clone() {
26518 Token::Ident(n) | Token::QuotedIdent(n) => {
26519 self.advance();
26520 n
26521 }
26522 other => {
26523 return Err(self.err(format!(
26524 "expected element name after XMLELEMENT NAME, got {other:?}"
26525 )));
26526 }
26527 };
26528 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26529 while matches!(self.peek(), Token::Comma) {
26530 self.advance();
26531 args.push(self.parse_expr(0)?);
26532 }
26533 if !matches!(self.peek(), Token::RParen) {
26534 return Err(self.err(format!(
26535 "expected ')' to close XMLELEMENT, got {:?}",
26536 self.peek()
26537 )));
26538 }
26539 self.advance();
26540 return Ok(Expr::FunctionCall {
26541 name: String::from("xmlelement"),
26542 args,
26543 });
26544 }
26545 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26546 // becomes a `<name>value</name>` element; a bare column infers its
26547 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26548 // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26549 // `CONVERT(expr USING cs)` was a syntax error at USING, and
26550 // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26551 // `convert(bytea, src, dest)` and answered `column "char" does
26552 // not exist`. Both are casts in MySQL: measured on 9.7.2,
26553 // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26554 // both 'A', and `CONVERT(123, CHAR)` is '123'.
26555 //
26556 // The charset is checked against the same table the introducers
26557 // use, so an unknown one is refused rather than quietly ignored.
26558 if self.mysql_dialect
26559 && first.eq_ignore_ascii_case("convert")
26560 && !matches!(self.peek(), Token::RParen)
26561 {
26562 let save = self.pos;
26563 let inner = self.parse_expr(0)?;
26564 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26565 self.advance();
26566 let cs = match self.peek().clone() {
26567 Token::Ident(n) | Token::QuotedIdent(n) => {
26568 self.advance();
26569 n
26570 }
26571 other => {
26572 return Err(self.err(alloc::format!(
26573 "expected a charset after USING, got {other:?}"
26574 )));
26575 }
26576 };
26577 let lc = cs.to_ascii_lowercase();
26578 if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26579 return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26580 }
26581 if !matches!(self.peek(), Token::RParen) {
26582 return Err(self.err(alloc::format!(
26583 "expected ')' after CONVERT … USING, got {:?}",
26584 self.peek()
26585 )));
26586 }
26587 self.advance();
26588 let target = if lc == "binary" {
26589 CastTarget::Named("binary".to_string())
26590 } else {
26591 CastTarget::Text
26592 };
26593 return self.finish_postfix_casts(Expr::Cast {
26594 expr: alloc::boxed::Box::new(inner),
26595 target,
26596 });
26597 }
26598 if matches!(self.peek(), Token::Comma) {
26599 self.advance();
26600 // A type name here is MySQL's cast form; anything else
26601 // (three string arguments) is PostgreSQL's `convert`,
26602 // which keeps its own path.
26603 if let Ok(target) = self.parse_cast_target()
26604 && matches!(self.peek(), Token::RParen)
26605 {
26606 self.advance();
26607 return self.finish_postfix_casts(Expr::Cast {
26608 expr: alloc::boxed::Box::new(inner),
26609 target,
26610 });
26611 }
26612 }
26613 self.pos = save;
26614 }
26615 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26616 let mut args: Vec<Expr> = Vec::new();
26617 loop {
26618 let val = self.parse_expr(0)?;
26619 let name = if matches!(self.peek(), Token::As) {
26620 self.advance();
26621 match self.peek().clone() {
26622 Token::Ident(n) | Token::QuotedIdent(n) => {
26623 self.advance();
26624 n
26625 }
26626 other => {
26627 return Err(self.err(format!(
26628 "expected name after AS in XMLFOREST, got {other:?}"
26629 )));
26630 }
26631 }
26632 } else if let Expr::Column(c) = &val {
26633 c.name.clone()
26634 } else {
26635 return Err(
26636 self.err("XMLFOREST element without a column name needs AS".into())
26637 );
26638 };
26639 args.push(Expr::Literal(Literal::String(name)));
26640 args.push(val);
26641 if matches!(self.peek(), Token::Comma) {
26642 self.advance();
26643 } else {
26644 break;
26645 }
26646 }
26647 if !matches!(self.peek(), Token::RParen) {
26648 return Err(self.err(format!(
26649 "expected ')' to close XMLFOREST, got {:?}",
26650 self.peek()
26651 )));
26652 }
26653 self.advance();
26654 return Ok(Expr::FunctionCall {
26655 name: String::from("xmlforest"),
26656 args,
26657 });
26658 }
26659 // SQL-standard `POSITION(sub IN str)` — lowers onto
26660 // strpos(str, sub). IN is the argument separator here,
26661 // so the needle parses with the IN-tail suppressed.
26662 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26663 let saved = self.suppress_in_tail;
26664 self.suppress_in_tail = true;
26665 let needle = self.parse_expr(0);
26666 self.suppress_in_tail = saved;
26667 let needle = needle?;
26668 if matches!(self.peek(), Token::In) {
26669 self.advance();
26670 let haystack = self.parse_expr(0)?;
26671 if !matches!(self.peek(), Token::RParen) {
26672 return Err(self.err(format!(
26673 "expected ')' to close POSITION, got {:?}",
26674 self.peek()
26675 )));
26676 }
26677 self.advance();
26678 return Ok(Expr::FunctionCall {
26679 name: String::from("strpos"),
26680 args: alloc::vec![haystack, needle],
26681 });
26682 }
26683 // position(sub, str) comma form (incl. bytea) —
26684 // hand the parsed first arg to the generic list.
26685 args.push(needle);
26686 if matches!(self.peek(), Token::Comma) {
26687 self.advance();
26688 }
26689 }
26690 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26691 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26692 // plain comma forms TRIM(str) / TRIM(str, chars) keep
26693 // riding the generic argument list below.
26694 if first.eq_ignore_ascii_case("trim") {
26695 let mode = match self.peek() {
26696 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26697 self.advance();
26698 Some("btrim")
26699 }
26700 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26701 self.advance();
26702 Some("ltrim")
26703 }
26704 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26705 self.advance();
26706 Some("rtrim")
26707 }
26708 _ => None,
26709 };
26710 if mode.is_some() || matches!(self.peek(), Token::From) {
26711 // TRIM([mode] FROM str) — no strip-chars.
26712 let chars = if matches!(self.peek(), Token::From) {
26713 None
26714 } else {
26715 Some(self.parse_expr(0)?)
26716 };
26717 if !matches!(self.peek(), Token::From) {
26718 return Err(self.err(format!(
26719 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26720 self.peek()
26721 )));
26722 }
26723 self.advance();
26724 let target = self.parse_expr(0)?;
26725 if !matches!(self.peek(), Token::RParen) {
26726 return Err(
26727 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26728 );
26729 }
26730 self.advance();
26731 let mut trim_args = alloc::vec![target];
26732 if let Some(c) = chars {
26733 trim_args.push(c);
26734 }
26735 return Ok(Expr::FunctionCall {
26736 name: String::from(mode.unwrap_or("btrim")),
26737 args: trim_args,
26738 });
26739 }
26740 }
26741 if !matches!(self.peek(), Token::RParen) {
26742 loop {
26743 // v7.38 (read01, T14) — `argname => value` names this arg.
26744 // v7.39 (read01 round 77) — `argname := value` is the same
26745 // thing, and it is the spelling PG's own docs lead with. It
26746 // was simply never lexed here, so every `f(x := 1)` died in
26747 // the parser regardless of what `f` was.
26748 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26749 (
26750 Token::Ident(n) | Token::QuotedIdent(n),
26751 Some(Token::FatArrow | Token::ColonEq),
26752 ) => {
26753 let name = n.clone();
26754 self.advance(); // name
26755 self.advance(); // => / :=
26756 Some(name)
26757 }
26758 _ => None,
26759 };
26760 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26761 // array's elements into a variadic call's trailing args
26762 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26763 // reserved, so it arrives as a bare ident before the arg.
26764 let is_variadic = this_name.is_none()
26765 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26766 if is_variadic {
26767 self.advance();
26768 }
26769 let arg = self.parse_expr(0)?;
26770 args.push(match &this_name {
26771 // The callee's parameter names decide the slot, and a
26772 // user function's live in the catalog. Carry the name
26773 // to eval rather than guessing here.
26774 Some(n) => Expr::NamedArg {
26775 name: n.clone(),
26776 expr: Box::new(arg),
26777 },
26778 None if is_variadic => Expr::Variadic(Box::new(arg)),
26779 None => arg,
26780 });
26781 arg_names.push(this_name);
26782 // v7.25 (round-17) — standard `CAST(expr AS type)`.
26783 // The `::` cast already worked; this lowers the
26784 // function form onto the same Expr::Cast node.
26785 if first.eq_ignore_ascii_case("cast")
26786 && args.len() == 1
26787 && matches!(self.peek(), Token::As)
26788 {
26789 self.advance();
26790 let target = self.parse_cast_target()?;
26791 if !matches!(self.peek(), Token::RParen) {
26792 return Err(self.err(format!(
26793 "expected ')' to close CAST, got {:?}",
26794 self.peek()
26795 )));
26796 }
26797 self.advance();
26798 return Ok(Expr::Cast {
26799 expr: Box::new(args.pop().expect("one arg")),
26800 target,
26801 });
26802 }
26803 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26804 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26805 // keywords; SPG's lexer makes them plain idents (so they'd be
26806 // read as column refs). Lower the keyword to the string form
26807 // the evaluator already accepts.
26808 if first.eq_ignore_ascii_case("normalize")
26809 && args.len() == 1
26810 && matches!(self.peek(), Token::Comma)
26811 {
26812 let form = match self.tokens.get(self.pos + 1) {
26813 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26814 let up = f.to_ascii_uppercase();
26815 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26816 }
26817 _ => None,
26818 };
26819 if let Some(up) = form {
26820 self.advance(); // comma
26821 self.advance(); // form keyword
26822 args.push(Expr::Literal(Literal::String(up)));
26823 }
26824 }
26825 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26826 // form. Desugars to the comma-list shape evaluator already
26827 // handles. Triggered after the first arg when the function
26828 // name is substring / substr and the next token is FROM
26829 // (a reserved keyword in PG; SPG also reserves it).
26830 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26831 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26832 // internal __substring_similar(str, pat, esc) call.
26833 if (first.eq_ignore_ascii_case("substring")
26834 || first.eq_ignore_ascii_case("substr"))
26835 && args.len() == 1
26836 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26837 {
26838 self.advance(); // SIMILAR
26839 let pattern = self.parse_expr(0)?;
26840 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26841 {
26842 return Err(self.err(format!(
26843 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26844 self.peek()
26845 )));
26846 }
26847 self.advance(); // ESCAPE
26848 let esc = self.parse_expr(0)?;
26849 if !matches!(self.peek(), Token::RParen) {
26850 return Err(self.err(format!(
26851 "expected ')' to close substring(... SIMILAR ...), got {:?}",
26852 self.peek()
26853 )));
26854 }
26855 self.advance();
26856 args.push(pattern);
26857 args.push(esc);
26858 return Ok(Expr::FunctionCall {
26859 name: "__substring_similar".to_string(),
26860 args,
26861 });
26862 }
26863 if (first.eq_ignore_ascii_case("substring")
26864 || first.eq_ignore_ascii_case("substr"))
26865 && args.len() == 1
26866 && matches!(self.peek(), Token::From | Token::For)
26867 {
26868 // `substring(str FROM pos [FOR len])`, or the FOR-only
26869 // `substring(str FOR len)` which PG treats as FROM 1.
26870 if matches!(self.peek(), Token::From) {
26871 self.advance();
26872 let start = self.parse_expr(0)?;
26873 args.push(start);
26874 } else {
26875 args.push(Expr::Literal(Literal::Integer(1)));
26876 }
26877 if matches!(self.peek(), Token::For) {
26878 self.advance();
26879 let length = self.parse_expr(0)?;
26880 args.push(length);
26881 }
26882 if !matches!(self.peek(), Token::RParen) {
26883 return Err(self.err(format!(
26884 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26885 self.peek()
26886 )));
26887 }
26888 self.advance();
26889 return Ok(Expr::FunctionCall {
26890 name: first.to_ascii_lowercase(),
26891 args,
26892 });
26893 }
26894 // PG `overlay(str PLACING repl FROM n [FOR len])`
26895 // syntactic form. Desugars to the `overlay(str,
26896 // repl, n[, len])` comma-list shape the evaluator
26897 // already implements. `PLACING` is not a reserved
26898 // token in SPG, so it arrives as a bare Ident.
26899 if first.eq_ignore_ascii_case("overlay")
26900 && args.len() == 1
26901 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26902 {
26903 self.advance(); // consume PLACING
26904 args.push(self.parse_expr(0)?); // replacement
26905 if !matches!(self.peek(), Token::From) {
26906 return Err(self.err(format!(
26907 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26908 self.peek()
26909 )));
26910 }
26911 self.advance();
26912 args.push(self.parse_expr(0)?); // start position
26913 if matches!(self.peek(), Token::For) {
26914 self.advance();
26915 args.push(self.parse_expr(0)?); // length
26916 }
26917 if !matches!(self.peek(), Token::RParen) {
26918 return Err(self.err(format!(
26919 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26920 self.peek()
26921 )));
26922 }
26923 self.advance();
26924 return Ok(Expr::FunctionCall {
26925 name: String::from("overlay"),
26926 args,
26927 });
26928 }
26929 // `TRIM(chars FROM str)` — the keyword-less
26930 // spelling lands here after the chars parse
26931 // (the keyword forms return earlier).
26932 if first.eq_ignore_ascii_case("trim")
26933 && args.len() == 1
26934 && matches!(self.peek(), Token::From)
26935 {
26936 self.advance();
26937 let target = self.parse_expr(0)?;
26938 if !matches!(self.peek(), Token::RParen) {
26939 return Err(self.err(format!(
26940 "expected ')' to close TRIM(chars FROM str), got {:?}",
26941 self.peek()
26942 )));
26943 }
26944 self.advance();
26945 let chars = args.pop().expect("one arg");
26946 return Ok(Expr::FunctionCall {
26947 name: String::from("btrim"),
26948 args: alloc::vec![target, chars],
26949 });
26950 }
26951 // v7.24 (round-16 A) — aggregate-internal
26952 // ordering: `array_agg(x ORDER BY y DESC NULLS
26953 // LAST)`. Keys close the argument list.
26954 if matches!(self.peek(), Token::Order) {
26955 self.advance();
26956 if !self.peek_is_by() {
26957 return Err(self.err(format!(
26958 "expected BY after ORDER in aggregate args, got {:?}",
26959 self.peek()
26960 )));
26961 }
26962 self.advance();
26963 loop {
26964 // v7.39 (round 691) — save/restore, the discipline this parser
26965 // already uses around `pending_sample_preds`, so a subquery inside
26966 // a key neither inherits nor leaks the channel.
26967 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26968 let saved_coll = self.order_key_collation.take();
26969 let parsed = self.parse_expr(0);
26970 self.in_order_by_key = saved_flag;
26971 let collation =
26972 core::mem::replace(&mut self.order_key_collation, saved_coll);
26973 let expr = parsed?;
26974 let desc = if matches!(self.peek(), Token::Desc) {
26975 self.advance();
26976 true
26977 } else if matches!(self.peek(), Token::Asc) {
26978 self.advance();
26979 false
26980 } else {
26981 false
26982 };
26983 let nulls_first = self.parse_optional_nulls_placement()?;
26984 agg_order_by.push(OrderBy {
26985 expr,
26986 desc,
26987 nulls_first,
26988 collation,
26989 });
26990 if matches!(self.peek(), Token::Comma) {
26991 self.advance();
26992 } else {
26993 break;
26994 }
26995 }
26996 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26997 // follow the ORDER BY inside GROUP_CONCAT.
26998 if self.consume_group_concat_separator(&mut args)? {
26999 saw_separator = true;
27000 }
27001 if !matches!(self.peek(), Token::RParen) {
27002 return Err(self.err(format!(
27003 "expected ')' after aggregate ORDER BY, got {:?}",
27004 self.peek()
27005 )));
27006 }
27007 break;
27008 }
27009 // v7.39 (round 354, M12) — …or directly after the
27010 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
27011 // own spelling of what PG passes as string_agg's second
27012 // argument; it was a parse error, so every MySQL query
27013 // that names its own separator failed outright.
27014 if self.consume_group_concat_separator(&mut args)? {
27015 saw_separator = true;
27016 break;
27017 }
27018 match self.peek() {
27019 Token::Comma => {
27020 self.advance();
27021 }
27022 Token::RParen => break,
27023 other => {
27024 return Err(self.err(format!(
27025 "expected ',' or ')' in function args, got {other:?}"
27026 )));
27027 }
27028 }
27029 }
27030 }
27031 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
27032 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
27033 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
27034 // meaning a separator — that is what the explicit SEPARATOR
27035 // tail is for. Fold them into one `concat(...)` so the
27036 // aggregate keeps its single value argument.
27037 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
27038 let values = args.len() - usize::from(saw_separator);
27039 if values > 1 {
27040 let sep_arg = if saw_separator { args.pop() } else { None };
27041 let folded = Expr::FunctionCall {
27042 name: "concat".to_string(),
27043 args: core::mem::take(&mut args),
27044 };
27045 args.push(folded);
27046 if let Some(sep) = sep_arg {
27047 args.push(sep);
27048 }
27049 }
27050 }
27051 self.advance(); // consume ')'
27052 // v7.39 (read01 round 77) — named arguments are NOT reordered here
27053 // any more. The parser has no catalog, so it could only ever resolve
27054 // the handful of `make_*` builtins whose parameter names were baked
27055 // into a table right here — every user function got
27056 // "does not support named arguments", though the catalog has been
27057 // storing its parameter names all along. Reordering happens in eval,
27058 // in one place, for builtins and user functions alike.
27059 // v7.32 (round-29) — ordered-set aggregate tail
27060 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
27061 // (percentile_cont / percentile_disc / mode). The sort spec
27062 // lands in the same `order_by` slot a decorated aggregate
27063 // uses; the executor dispatches on the function name. WITHIN
27064 // GROUP and an intra-argument ORDER BY are mutually
27065 // exclusive (PG rejects both).
27066 let within_group_order = self.parse_within_group_clause()?;
27067 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
27068 return Err(self.err(
27069 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
27070 .into(),
27071 ));
27072 }
27073 let within_group_seen = !within_group_order.is_empty();
27074 let agg_order_by = if within_group_order.is_empty() {
27075 agg_order_by
27076 } else {
27077 within_group_order
27078 };
27079 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
27080 let filter = self.parse_filter_clause()?;
27081 // v4.12: window-function tail — `name(args) OVER (...)`.
27082 // Promotes the just-parsed FunctionCall into a
27083 // WindowFunction node carrying partition + order.
27084 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
27085 // / `RESPECT NULLS OVER (...)` between the closing paren
27086 // and `OVER`.
27087 let null_treatment = self.parse_null_treatment_modifier();
27088 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
27089 && s.eq_ignore_ascii_case("over")
27090 {
27091 self.advance();
27092 // v7.39 (round 230) — PG implements neither modifier for a
27093 // windowed call and says so (0A000). Both used to be parsed
27094 // and then silently dropped here, so `count(DISTINCT v)
27095 // OVER (…)` quietly answered the non-distinct count.
27096 if agg_distinct {
27097 return Err(
27098 self.err("DISTINCT is not implemented for window functions".to_string())
27099 );
27100 }
27101 if !agg_order_by.is_empty() {
27102 // PG separates the two shapes that land here: a
27103 // WITHIN GROUP call is an ordered-set aggregate and gets
27104 // its own message naming the aggregate; a plain
27105 // `agg(x ORDER BY y)` gets the generic one.
27106 let msg = if within_group_seen {
27107 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
27108 } else {
27109 "aggregate ORDER BY is not implemented for window functions".to_string()
27110 };
27111 return Err(self.err(msg));
27112 }
27113 let (partition_by, order_by, frame) = self.parse_over_clause()?;
27114 return Ok(Expr::WindowFunction {
27115 name: first,
27116 args,
27117 partition_by,
27118 order_by,
27119 frame,
27120 null_treatment,
27121 filter,
27122 });
27123 }
27124 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
27125 return Ok(Expr::AggregateOrdered {
27126 call: Box::new(Expr::FunctionCall { name: first, args }),
27127 order_by: agg_order_by,
27128 distinct: agg_distinct,
27129 filter,
27130 });
27131 }
27132 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
27133 // over TIMESTAMPTZ and has no timestamp overload, so a
27134 // timestamp argument is coerced on the way in and the answer
27135 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
27136 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
27137 // zone`. SPG answered `timestamp without time zone`, dropping
27138 // the offset from every rendering.
27139 //
27140 // Writing the coercion PG performs makes the existing
27141 // argument-driven typing (the one `date_trunc` uses) reach the
27142 // right answer, rather than teaching the type layer a second
27143 // rule. MySQL's DATE_ADD is a different function that returns
27144 // DATE or DATETIME, so this is PG-dialect only.
27145 //
27146 // Out-of-line because this sits on the RECURSIVE descent
27147 // frame: an inline block with locals here costs every nesting
27148 // level, and the suite's deep-nesting sentinel overflowed the
27149 // 512 KiB parser stack the moment one was added (round 430's
27150 // lesson, in the same shape).
27151 if !self.mysql_dialect {
27152 lift_date_add_arg_to_timestamptz(&first, &mut args);
27153 }
27154 return Ok(Expr::FunctionCall { name: first, args });
27155 }
27156 // v7.9.20 — SQL-standard parenless keyword expressions
27157 // (PG treats these as functions called without parens).
27158 // Resolve to a synthetic FunctionCall so the engine's
27159 // eval path reuses the existing function-call routing.
27160 // mailrs G3.
27161 let lc = first.to_ascii_lowercase();
27162 if matches!(
27163 lc.as_str(),
27164 "current_date"
27165 | "current_time"
27166 | "current_timestamp"
27167 | "localtimestamp"
27168 | "localtime"
27169 // v7.37.17 (17.6 siblings) — session-identity SQL-
27170 // standard parenless keywords. current_user /
27171 // session_user / user were already caught by the
27172 // pgwire canned-response shortcut but bare-select
27173 // in the embedded engine went through Expr::Column
27174 // and errored. Adding them here so the parser
27175 // resolves to a synthetic FunctionCall that reuses
27176 // the existing eval/functions.rs dispatch.
27177 | "current_user"
27178 | "session_user"
27179 | "current_role"
27180 | "current_catalog"
27181 | "current_schema"
27182 | "current_database"
27183 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27184 | "system_user"
27185 ) {
27186 return Ok(Expr::FunctionCall {
27187 name: lc,
27188 args: Vec::new(),
27189 });
27190 }
27191 Ok(Expr::Column(ColumnName {
27192 qualifier: None,
27193 name: first,
27194 }))
27195 }
27196}
27197
27198/// v7.39 (round 522) — write the coercion PG's `date_add` /
27199/// `date_subtract` signature performs.
27200///
27201/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27202/// timestamp argument is cast on the way in and the answer is
27203/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27204/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27205/// `timestamp without time zone`, dropping the offset from every
27206/// rendering of the result.
27207///
27208/// Writing the cast the signature implies lets the existing
27209/// argument-driven typing (the one `date_trunc` uses) reach the right
27210/// answer instead of teaching the type layer a second rule. MySQL's
27211/// DATE_ADD is a different function returning DATE or DATETIME, so the
27212/// caller applies this in PG dialect only.
27213///
27214/// A free function, and not a block at the call site, because the caller
27215/// is on the recursive-descent frame chain.
27216#[inline(never)]
27217fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27218 if args.len() != 2
27219 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27220 {
27221 return;
27222 }
27223 let base = args.remove(0);
27224 args.insert(
27225 0,
27226 Expr::Cast {
27227 expr: Box::new(base),
27228 target: CastTarget::Timestamptz,
27229 },
27230 );
27231}
27232
27233/// v6.8.2 — walk an expression tree and return the first column
27234/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27235/// to derive `CreateIndexStatement.column` from an expression
27236/// key (so downstream planner code resolving a primary column
27237/// position keeps working with expression indexes). Returns
27238/// `None` when the expression has no column ref at all — caller
27239/// surfaces that as a parse error.
27240fn extract_first_column(expr: &Expr) -> Option<String> {
27241 match expr {
27242 Expr::Column(cn) => Some(cn.name.clone()),
27243 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27244 Expr::Binary { lhs, rhs, .. } => {
27245 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27246 }
27247 Expr::Unary { expr: e, .. } => extract_first_column(e),
27248 // v7.39 (read01 round 93) — a cast wraps its operand: a common
27249 // expression-index key is `lower(col::text)`, where the column
27250 // sits under the `::text` cast inside the function arg. Without
27251 // descending here the key was rejected as "references no column".
27252 Expr::Cast { expr: e, .. } => extract_first_column(e),
27253 // v7.39.2 — and a COLLATE wraps its operand the same way.
27254 // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27255 // column the moment the clause became a node instead of being
27256 // absorbed, and the key was rejected as referencing none. This
27257 // is the shape the wildcard below silently produces, which is
27258 // why it is spelled out.
27259 Expr::Collate { expr: e, .. } => extract_first_column(e),
27260 _ => None,
27261 }
27262}
27263
27264fn maybe_not(expr: Expr, negated: bool) -> Expr {
27265 if negated {
27266 Expr::Unary {
27267 op: UnOp::Not,
27268 expr: Box::new(expr),
27269 }
27270 } else {
27271 expr
27272 }
27273}
27274
27275/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27276/// things in the two dialects, and SPG read all three PG's way:
27277///
27278/// | token | PG (and SPG) | MySQL, measured |
27279/// |---|---|---|
27280/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27281/// | `&&` | inet / array overlap | **AND** |
27282/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27283///
27284/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27285/// answer with no error, which is why they are routed here rather than
27286/// left to the shared table.
27287impl Parser {
27288 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27289 if self.mysql_dialect {
27290 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27291 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27292 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27293 if let Token::Ident(w) = tok
27294 && w.eq_ignore_ascii_case("div")
27295 {
27296 return Some((BinOp::IntDiv, 8));
27297 }
27298 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27299 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27300 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27301 // there sits in operand position, not infix).
27302 if let Token::Ident(w) = tok
27303 && w.eq_ignore_ascii_case("mod")
27304 {
27305 return Some((BinOp::Mod, 8));
27306 }
27307 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27308 // plain ident to the lexer. Its precedence sits between OR (1)
27309 // and AND (3) — hence rung 2, the slot freed by moving AND up.
27310 if let Token::Ident(w) = tok
27311 && w.eq_ignore_ascii_case("xor")
27312 {
27313 return Some((BinOp::LogicalXor, 2));
27314 }
27315 match tok {
27316 Token::Concat => return Some((BinOp::Or, 1)),
27317 // MySQL's `&&` is logical AND, sharing AND's rung (3).
27318 Token::InetOverlap => return Some((BinOp::And, 3)),
27319 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27320 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27321 _ => {}
27322 }
27323 }
27324 binop_from(tok)
27325 }
27326}
27327
27328// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27329// (which sits strictly between OR and AND), every level from AND upward was
27330// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27331// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27332// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27333// the *relative* order of every PG operator is unchanged by the shift.
27334fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27335 let pair = match tok {
27336 Token::Or => (BinOp::Or, 1),
27337 Token::And => (BinOp::And, 3),
27338 Token::Eq => (BinOp::Eq, 5),
27339 Token::NotEq => (BinOp::NotEq, 5),
27340 Token::Lt => (BinOp::Lt, 5),
27341 Token::LtEq => (BinOp::LtEq, 5),
27342 Token::Gt => (BinOp::Gt, 5),
27343 Token::GtEq => (BinOp::GtEq, 5),
27344 // pgvector distance ops all sit on the same rung — tighter than
27345 // comparisons (5) so `col <-> v < threshold` parses correctly.
27346 Token::L2Distance => (BinOp::L2Distance, 6),
27347 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27348 // comparison rung.
27349 Token::GeomParallel => (BinOp::GeomParallel, 5),
27350 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27351 // comparison rung.
27352 Token::OverLeft => (BinOp::OverLeft, 5),
27353 Token::OverRight => (BinOp::OverRight, 5),
27354 Token::GeomPerp => (BinOp::GeomPerp, 5),
27355 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27356 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27357 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27358 Token::InnerProduct => (BinOp::InnerProduct, 6),
27359 Token::CosineDistance => (BinOp::CosineDistance, 6),
27360 Token::Plus => (BinOp::Add, 7),
27361 Token::Minus => (BinOp::Sub, 7),
27362 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27363 // binds every "other" operator (`||`, `|`, `&`, `#`, the
27364 // pgvector distances above) BETWEEN additive (7) and the
27365 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27366 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27367 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27368 // ("matches PG conceptually" — the round-753 audit measured it
27369 // false; the old rung errored on `'a' || 1 + 1` with
27370 // `text + integer`). Same-level chains left-fold, as PG does.
27371 Token::Concat => (BinOp::Concat, 6),
27372 Token::Pipe => (BinOp::BitOr, 6),
27373 Token::Amp => (BinOp::BitAnd, 6),
27374 Token::Star => (BinOp::Mul, 8),
27375 Token::Slash => (BinOp::Div, 8),
27376 Token::Percent => (BinOp::Mod, 8),
27377 // v4.14: JSON path ops bind tighter than comparisons (5)
27378 // and additive (7) so `doc->'k' = 'v'` parses correctly.
27379 // Same rung as the multiplicative ops.
27380 Token::JsonGet => (BinOp::JsonGet, 8),
27381 Token::JsonGetText => (BinOp::JsonGetText, 8),
27382 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27383 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27384 Token::JsonContains => (BinOp::JsonContains, 8),
27385 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27386 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27387 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27388 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27389 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27390 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27391 // v7.12.2 — `@@` binds at the comparison rung (looser than
27392 // arithmetic, tighter than AND / OR). PG places `@@` at
27393 // the same precedence as `=` / `<`, so we follow.
27394 Token::TsMatch => (BinOp::TsMatch, 5),
27395 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27396 // PG places these at the comparison rung (same level as `=`),
27397 // so we follow.
27398 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27399 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27400 Token::InetContains => (BinOp::InetContains, 5),
27401 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27402 Token::InetOverlap => (BinOp::InetOverlap, 5),
27403 // v7.39 (round 508) — the geometric and pattern-order predicates
27404 // ride the comparison rung, as every other predicate does.
27405 Token::Intersects => (BinOp::Intersects, 5),
27406 Token::IsBelow => (BinOp::IsBelow, 5),
27407 Token::IsAbove => (BinOp::IsAbove, 5),
27408 Token::PatternLt => (BinOp::PatternLt, 5),
27409 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27410 Token::PatternGt => (BinOp::PatternGt, 5),
27411 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27412 // `@@@` is the old spelling of `@@` and means exactly it.
27413 Token::TsMatchOld => (BinOp::TsMatch, 5),
27414 _ => return None,
27415 };
27416 Some(pair)
27417}
27418
27419#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27420// `as f32` here is intentional: vector elements widen / narrow into f32 on
27421// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27422// past ~15 decimal digits — both are acceptable for a fixed-precision
27423// pgvector column.
27424/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27425/// implicit table alias and break trailing clauses. WITH lands
27426/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27427/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27428/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27429/// / VALUES / FOR / LATERAL — all of which would otherwise be
27430/// silently swallowed by `parse_optional_alias`.
27431fn is_alias_stopword(s: &str) -> bool {
27432 matches!(
27433 s.to_ascii_lowercase().as_str(),
27434 "with"
27435 | "on"
27436 | "where"
27437 | "having"
27438 | "group"
27439 | "order"
27440 | "limit"
27441 | "offset"
27442 | "union"
27443 | "except"
27444 | "intersect"
27445 | "returning"
27446 | "set"
27447 | "values"
27448 | "for"
27449 | "window"
27450 | "tablesample"
27451 | "lateral"
27452 | "left"
27453 | "right"
27454 | "inner"
27455 | "outer"
27456 | "full"
27457 | "cross"
27458 | "join"
27459 | "natural"
27460 | "using"
27461 | "fetch"
27462 )
27463}
27464
27465fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27466 match e {
27467 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27468 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27469 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27470 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27471 // so scale the divisor by hand instead of `f32::powi`.)
27472 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27473 let mut div = 1.0f32;
27474 for _ in 0..*scale {
27475 div *= 10.0;
27476 }
27477 Some(*unscaled as f32 / div)
27478 }
27479 Expr::Unary {
27480 op: UnOp::Neg,
27481 expr,
27482 } => extract_numeric_literal(expr).map(|x| -x),
27483 _ => None,
27484 }
27485}
27486
27487/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27488/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27489/// negative. Returns `None` if any pair fails to parse or no pair is found.
27490///
27491/// Recognised units (case-insensitive, optional trailing `s`):
27492/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27493/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27494/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27495/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27496/// (PG-canonical: DST and month-boundary semantics depend on this).
27497/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27498/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27499/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27500#[allow(clippy::cast_possible_truncation)]
27501fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27502 let mut months: i64 = 0;
27503 let mut days: i64 = 0;
27504 let mut micros: i64 = 0;
27505 let mut in_time = false;
27506 let mut num = alloc::string::String::new();
27507 for ch in rest.chars() {
27508 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27509 num.push(ch);
27510 continue;
27511 }
27512 if ch == 'T' || ch == 't' {
27513 if !num.is_empty() {
27514 return None;
27515 }
27516 in_time = true;
27517 continue;
27518 }
27519 let n: f64 = num.parse().ok()?;
27520 num.clear();
27521 match (ch, in_time) {
27522 ('Y' | 'y', false) => months += (n * 12.0) as i64,
27523 ('M', false) => months += n as i64,
27524 ('W' | 'w', false) => days += (n * 7.0) as i64,
27525 ('D' | 'd', false) => days += n as i64,
27526 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27527 ('M', true) => micros += (n * 60_000_000.0) as i64,
27528 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27529 _ => return None,
27530 }
27531 }
27532 if !num.is_empty() {
27533 return None;
27534 }
27535 Some((
27536 i32::try_from(months).ok()?,
27537 i32::try_from(days).ok()?,
27538 micros,
27539 ))
27540}
27541
27542/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27543/// leading `-` negates the whole value). Rejects date-like strings.
27544fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27545 let (neg, body) = match s.strip_prefix('-') {
27546 Some(b) => (true, b),
27547 None => (false, s),
27548 };
27549 let (y, m) = body.split_once('-')?;
27550 let years: i32 = y.parse().ok()?;
27551 let mons: i32 = m.parse().ok()?;
27552 if years < 0 || mons < 0 {
27553 return None;
27554 }
27555 let total = years.checked_mul(12)?.checked_add(mons)?;
27556 Some((if neg { -total } else { total }, 0, 0))
27557}
27558
27559/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27560/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27561fn parse_interval_clock(tok: &str) -> Option<i64> {
27562 let (neg, body) = match tok.strip_prefix('-') {
27563 Some(r) => (true, r),
27564 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27565 };
27566 let mut it = body.split(':');
27567 let h: i64 = it.next()?.parse().ok()?;
27568 let m: i64 = it.next()?.parse().ok()?;
27569 let s_tok = it.next().unwrap_or("0");
27570 if it.next().is_some() {
27571 return None;
27572 }
27573 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27574 let sec: i64 = sec.parse().ok()?;
27575 let mut f = alloc::string::String::from(frac);
27576 while f.len() < 6 {
27577 f.push('0');
27578 }
27579 f.truncate(6);
27580 let fus: i64 = f.parse().ok()?;
27581 sec.checked_mul(1_000_000)?.checked_add(fus)?
27582 } else {
27583 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27584 };
27585 let total = h
27586 .checked_mul(3_600_000_000)?
27587 .checked_add(m.checked_mul(60_000_000)?)?
27588 .checked_add(sec_us)?;
27589 Some(if neg { -total } else { total })
27590}
27591
27592/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27593/// every spelling PG accepts (measured against live PG18.4, not guessed):
27594/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27595/// Before this, the unit table matched long names only, with an ad-hoc
27596/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27597/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27598/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27599/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27600/// fractional) both read from this one table now.
27601fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27602 let u = raw.to_ascii_lowercase();
27603 Some(match u.as_str() {
27604 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27605 "microsecond"
27606 }
27607 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27608 "millisecond"
27609 }
27610 "second" | "seconds" | "sec" | "secs" | "s" => "second",
27611 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27612 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27613 "day" | "days" | "d" => "day",
27614 "week" | "weeks" | "w" => "week",
27615 "month" | "months" | "mon" | "mons" => "month",
27616 "year" | "years" | "yr" | "yrs" | "y" => "year",
27617 "decade" | "decades" | "dec" | "decs" => "decade",
27618 "century" | "centuries" | "cent" | "c" => "century",
27619 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27620 _ => return None,
27621 })
27622}
27623
27624/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27625/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27627pub(crate) enum IntervalField {
27628 Year,
27629 Month,
27630 Day,
27631 Hour,
27632 Minute,
27633 Second,
27634}
27635
27636/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27637/// spellings aren't standard for the qualifier position, so only the singular
27638/// forms are accepted.
27639/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27640/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27641/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27642/// take a `'1 2'` style literal — are not read here; they stay a parse
27643/// error rather than being silently misread.)
27644/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27645///
27646/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27647/// to do with a `@@` engine setting, and an unset one reads NULL rather
27648/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27649/// were the same node and `SELECT @x` answered "Unknown system variable".)
27650/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27651/// not see a session override — measured, after `SET autocommit=0`,
27652/// `@@global.autocommit` is still 1.
27653///
27654/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27655/// the parser's nesting budget is tuned against, and building these
27656/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27657/// wall `parse_left_right_atom` and friends were factored out for).
27658#[inline(never)]
27659fn variable_ref_atom(raw: &str) -> Expr {
27660 let user_var = !raw.starts_with("@@");
27661 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27662 Expr::FunctionCall {
27663 name: String::from(if user_var {
27664 "__spg_user_var"
27665 } else {
27666 "__spg_session_var"
27667 }),
27668 args: alloc::vec![Expr::Literal(Literal::String(bare))],
27669 }
27670}
27671
27672fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27673 let Token::Ident(s) = tok else { return None };
27674 Some(match () {
27675 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27676 () if s.eq_ignore_ascii_case("second") => "second",
27677 () if s.eq_ignore_ascii_case("minute") => "minute",
27678 () if s.eq_ignore_ascii_case("hour") => "hour",
27679 () if s.eq_ignore_ascii_case("day") => "day",
27680 () if s.eq_ignore_ascii_case("week") => "week",
27681 () if s.eq_ignore_ascii_case("month") => "month",
27682 () if s.eq_ignore_ascii_case("quarter") => "quarter",
27683 () if s.eq_ignore_ascii_case("year") => "year",
27684 () => return None,
27685 })
27686}
27687
27688/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27689/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27690/// which constructs the value at run time. Only the slot the unit names
27691/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27692/// slot the builtin has (months and fractional seconds respectively).
27693fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27694 let zero = || Expr::Literal(Literal::Integer(0));
27695 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27696 lhs: alloc::boxed::Box::new(qty.clone()),
27697 op,
27698 rhs: alloc::boxed::Box::new(by),
27699 };
27700 // (years, months, weeks, days, hours, mins, secs)
27701 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27702 match unit {
27703 "year" => args[0] = qty,
27704 "quarter" => {
27705 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27706 }
27707 "month" => args[1] = qty,
27708 "week" => args[2] = qty,
27709 "day" => args[3] = qty,
27710 "hour" => args[4] = qty,
27711 "minute" => args[5] = qty,
27712 "second" => args[6] = qty,
27713 // The builtin's seconds slot takes a fraction, so microseconds ride
27714 // it scaled down; the divisor is a NUMERIC literal so the division
27715 // stays exact rather than going through a float.
27716 "microsecond" => {
27717 args[6] = scaled(
27718 crate::ast::BinOp::Div,
27719 Expr::Literal(Literal::Numeric {
27720 unscaled: 1_000_000,
27721 scale: 0,
27722 }),
27723 );
27724 }
27725 _ => args[3] = qty,
27726 }
27727 Expr::FunctionCall {
27728 name: alloc::string::String::from("make_interval"),
27729 args,
27730 }
27731}
27732
27733/// `(count, unit)` → `(months, days, micros)`.
27734fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27735 let n: i64 = count.trim().parse().ok()?;
27736 Some(match unit {
27737 "microsecond" => (0, 0, n),
27738 "second" => (0, 0, n.checked_mul(1_000_000)?),
27739 "minute" => (0, 0, n.checked_mul(60_000_000)?),
27740 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27741 "day" => (0, i32::try_from(n).ok()?, 0),
27742 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27743 "month" => (i32::try_from(n).ok()?, 0, 0),
27744 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27745 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27746 _ => return None,
27747 })
27748}
27749
27750fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27751 let Token::Ident(s) = tok else { return None };
27752 Some(match () {
27753 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27754 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27755 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27756 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27757 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27758 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27759 () => return None,
27760 })
27761}
27762
27763/// v7.39 (read01 round 102) — interpret an interval literal under a field
27764/// qualifier. Returns `(months, days, micros)`.
27765///
27766/// * A single field applied to a bare number sets which unit the number means,
27767/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27768/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27769/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27770/// * Every other range, and any literal a single field can't read as a plain
27771/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27772/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27773/// like PG, and the qualifier there only bounds precision.
27774fn interpret_qualified_interval(
27775 text: &str,
27776 (f1, f2): (IntervalField, Option<IntervalField>),
27777) -> Option<(i32, i32, i64)> {
27778 if let Some(f2) = f2 {
27779 if f1 == IntervalField::Year && f2 == IntervalField::Month {
27780 if let Some(m) = parse_year_month_literal(text) {
27781 return Some((m, 0, 0));
27782 }
27783 }
27784 return parse_interval_text(text);
27785 }
27786 // Single field: reinterpret a bare number; otherwise the default parse.
27787 let trimmed = text.trim();
27788 if let Ok(val) = trimmed.parse::<f64>() {
27789 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27790 #[allow(clippy::cast_possible_truncation)]
27791 let whole = val as i64;
27792 #[allow(clippy::cast_possible_truncation)]
27793 let secs_micros = {
27794 let m = val * 1_000_000.0;
27795 if m >= 0.0 {
27796 (m + 0.5) as i64
27797 } else {
27798 (m - 0.5) as i64
27799 }
27800 };
27801 return Some(match f1 {
27802 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27803 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27804 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27805 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27806 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27807 IntervalField::Second => (0, 0, secs_micros),
27808 });
27809 }
27810 parse_interval_text(text)
27811}
27812
27813/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27814fn parse_year_month_literal(text: &str) -> Option<i32> {
27815 let t = text.trim();
27816 let (neg, body) = match t.strip_prefix('-') {
27817 Some(r) => (true, r),
27818 None => (false, t.strip_prefix('+').unwrap_or(t)),
27819 };
27820 let mut it = body.split('-');
27821 let years: i32 = it.next()?.trim().parse().ok()?;
27822 let months: i32 = match it.next() {
27823 Some(m) => m.trim().parse().ok()?,
27824 None => 0,
27825 };
27826 if it.next().is_some() {
27827 return None;
27828 }
27829 let total = years.checked_mul(12)?.checked_add(months)?;
27830 Some(if neg { -total } else { total })
27831}
27832
27833pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27834 // v7.38.19 — the two infinities, answered as the three extreme
27835 // fields PostgreSQL itself puts on the wire for them:
27836 //
27837 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27838 // … 7fffffffffffffff 7fffffff 7fffffff
27839 //
27840 // So no caller has to know the spelling — every one of them already
27841 // reads the three numbers, and `IntervalKind::from_fields` names
27842 // what they mean.
27843 //
27844 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27845 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27846 // infinity. Interval takes the full word, in any case.
27847 {
27848 let word = s.trim();
27849 let word = word.strip_prefix('@').map_or(word, str::trim);
27850 let (neg, body) = match word.strip_prefix('-') {
27851 Some(rest) => (true, rest.trim_start()),
27852 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27853 };
27854 if body.eq_ignore_ascii_case("infinity") {
27855 return Some(if neg {
27856 (i32::MIN, i32::MIN, i64::MIN)
27857 } else {
27858 (i32::MAX, i32::MAX, i64::MAX)
27859 });
27860 }
27861 }
27862 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27863 // `@` is decorative; a trailing `ago` negates the whole interval.
27864 let mut trimmed = s.trim();
27865 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27866 let mut negate = false;
27867 if let Some(rest) = trimmed
27868 .strip_suffix("ago")
27869 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27870 {
27871 negate = true;
27872 trimmed = rest.trim();
27873 }
27874 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27875 let (mo, d, us) = v?;
27876 if negate {
27877 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27878 } else {
27879 Some((mo, d, us))
27880 }
27881 };
27882 let s = trimmed;
27883 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27884 // are single tokens, not the `<n> <unit>` pair form handled below.
27885 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27886 return finish(parse_iso8601_interval(rest));
27887 }
27888 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27889 if let Some(iv) = parse_year_month_interval(trimmed) {
27890 return finish(Some(iv));
27891 }
27892 }
27893 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27894 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27895 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27896 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27897 if let Ok(n) = trimmed.parse::<i64>() {
27898 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27899 }
27900 if let Ok(f) = trimmed.parse::<f64>() {
27901 if f.is_finite() {
27902 #[allow(clippy::cast_possible_truncation)]
27903 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27904 }
27905 }
27906 }
27907 // v7.39 (round 243) — PG accepts the number and unit run together
27908 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27909 // the `<n> <unit>` pair loop below sees them as two.
27910 let raw_parts: Vec<&str> = s.split_whitespace().collect();
27911 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27912 for p in raw_parts {
27913 let boundary = p
27914 .char_indices()
27915 .find(|(i, c)| {
27916 *i > 0
27917 && c.is_ascii_alphabetic()
27918 && p[..*i]
27919 .chars()
27920 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27921 && p[..*i].chars().any(|d| d.is_ascii_digit())
27922 })
27923 .map(|(i, _)| i);
27924 match boundary {
27925 Some(i) => {
27926 parts.push(&p[..i]);
27927 parts.push(&p[i..]);
27928 }
27929 None => parts.push(p),
27930 }
27931 }
27932 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27933 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27934 // remains is the `<n> <unit>` pair form handled below.
27935 let mut clock_us: i64 = 0;
27936 let mut had_clock = false;
27937 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27938 clock_us = parse_interval_clock(parts[pos])?;
27939 parts.remove(pos);
27940 had_clock = true;
27941 }
27942 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27943 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27944 let mut lone_days: i32 = 0;
27945 if had_clock && parts.len() == 1 {
27946 if let Ok(n) = parts[0].parse::<i64>() {
27947 lone_days = i32::try_from(n).ok()?;
27948 parts.clear();
27949 }
27950 }
27951 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27952 return None;
27953 }
27954 let mut months: i32 = 0;
27955 let mut days: i32 = lone_days;
27956 let mut micros: i64 = clock_us;
27957 let mut i = 0;
27958 while i < parts.len() {
27959 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27960 if let Ok(n) = parts[i].parse::<i64>() {
27961 match unit_stripped {
27962 "microsecond" => micros = micros.checked_add(n)?,
27963 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27964 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27965 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27966 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27967 "day" => {
27968 let n32 = i32::try_from(n).ok()?;
27969 days = days.checked_add(n32)?;
27970 }
27971 "week" => {
27972 let n32 = i32::try_from(n).ok()?;
27973 days = days.checked_add(n32.checked_mul(7)?)?;
27974 }
27975 "month" => {
27976 let n32 = i32::try_from(n).ok()?;
27977 months = months.checked_add(n32)?;
27978 }
27979 "year" => {
27980 let n32 = i32::try_from(n).ok()?;
27981 months = months.checked_add(n32.checked_mul(12)?)?;
27982 }
27983 // v7.39 (read01 timestamp.c) — the larger calendar units.
27984 "decade" => {
27985 let n32 = i32::try_from(n).ok()?;
27986 months = months.checked_add(n32.checked_mul(120)?)?;
27987 }
27988 "century" => {
27989 let n32 = i32::try_from(n).ok()?;
27990 months = months.checked_add(n32.checked_mul(1200)?)?;
27991 }
27992 "millennium" => {
27993 let n32 = i32::try_from(n).ok()?;
27994 months = months.checked_add(n32.checked_mul(12000)?)?;
27995 }
27996 _ => return None,
27997 }
27998 } else if let Ok(f) = parts[i].parse::<f64>() {
27999 // Fractional units cascade down to the next-finer field the way
28000 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
28001 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
28002 // no_std: f64 has no trunc/fract/round methods, so do them with
28003 // casts (toward-zero) + explicit round-half-away-from-zero.
28004 #[allow(clippy::cast_possible_truncation)]
28005 fn round_i64(x: f64) -> i64 {
28006 if x >= 0.0 {
28007 (x + 0.5) as i64
28008 } else {
28009 (x - 0.5) as i64
28010 }
28011 }
28012 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
28013 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
28014 const DAY_US: f64 = 86_400_000_000.0;
28015 let whole = d as i64; // truncates toward zero
28016 let frac = d - whole as f64;
28017 *days = days.checked_add(i32::try_from(whole).ok()?)?;
28018 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
28019 Some(())
28020 }
28021 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
28022 match unit_stripped {
28023 "microsecond" => micros = micros.checked_add(round_i64(f))?,
28024 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
28025 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
28026 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
28027 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
28028 "day" => add_days_frac(&mut days, &mut micros, f)?,
28029 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
28030 "month" => {
28031 let whole = f as i64;
28032 months = months.checked_add(i32::try_from(whole).ok()?)?;
28033 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
28034 }
28035 "year" => {
28036 let m = f * 12.0;
28037 let whole = m as i64;
28038 months = months.checked_add(i32::try_from(whole).ok()?)?;
28039 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
28040 }
28041 _ => return None,
28042 }
28043 } else {
28044 return None;
28045 }
28046 i += 2;
28047 }
28048 finish(Some((months, days, micros)))
28049}
28050
28051/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
28052/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
28053/// `interval` is intentionally absent (handled by its own parser arm).
28054/// Returns `None` for names that aren't sensible as a bare typed literal, so
28055/// the caller falls back to treating the ident as a column reference.
28056fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
28057 Some(match ident {
28058 "date" => CastTarget::Date,
28059 "timestamp" | "datetime" => CastTarget::Timestamp,
28060 "timestamptz" => CastTarget::Timestamptz,
28061 "bool" | "boolean" => CastTarget::Bool,
28062 "int" | "integer" | "int4" => CastTarget::Int,
28063 "bigint" | "int8" => CastTarget::BigInt,
28064 "float8" | "double precision" => CastTarget::Float,
28065 "uuid" => CastTarget::Uuid,
28066 "bytea" => CastTarget::Bytea,
28067 "json" => CastTarget::Json,
28068 "jsonb" => CastTarget::Jsonb,
28069 // Types without a dedicated CastTarget variant flow through the
28070 // generic Named path (engine resolves via column_type_to_data_type).
28071 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
28072 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
28073 | "money" | "bit" | "varbit"
28074 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
28075 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
28076 // Range / multirange types likewise.
28077 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
28078 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
28079 | "datemultirange" | "tsmultirange" | "tstzmultirange"
28080 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
28081 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
28082 CastTarget::Named(alloc::string::String::from(ident))
28083 }
28084 _ => return None,
28085 })
28086}
28087
28088/// v7.12.4 — map a bare type-name identifier (the form that
28089/// appears in a function arg list or RETURNS clause) to a
28090/// [`ColumnTypeName`]. Returns `None` for unknown / extension
28091/// types so the caller can preserve them as
28092/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
28093///
28094/// Subset of the full column-type grammar — we deliberately
28095/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
28096/// here because function-arg types in v7.12.4 are mostly the
28097/// bare form (`text`, `int`, `bytea`, …).
28098/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
28099/// than being `name TYPE`?
28100///
28101/// The multi-word spellings SQL allows for a bare argument type, each
28102/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
28103///
28104/// NOTE this list also exists in `spg-storage`, which computes the
28105/// signature key from the rendered argument text and has to reach the
28106/// same verdict. The two crates are siblings — neither depends on the
28107/// other — and each already carries its own table of type spellings
28108/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
28109/// there), so this follows the structure rather than inventing new
28110/// duplication. Recorded as V49.
28111pub fn is_multiword_type_phrase(phrase: &str) -> bool {
28112 let t = phrase.trim().to_ascii_lowercase();
28113 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
28114 matches!(
28115 base,
28116 "double precision"
28117 | "character varying"
28118 | "bit varying"
28119 | "timestamp with time zone"
28120 | "timestamp without time zone"
28121 | "time with time zone"
28122 | "time without time zone"
28123 | "national character"
28124 | "national character varying"
28125 )
28126}
28127
28128fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
28129 Some(match ident.to_ascii_lowercase().as_str() {
28130 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
28131 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
28132 "bigint" => ColumnTypeName::BigInt,
28133 "float" | "double" => ColumnTypeName::Float,
28134 // v7.39 (round 269) — real is 32-bit.
28135 "real" | "float4" => ColumnTypeName::Real,
28136 "text" => ColumnTypeName::Text,
28137 "bool" | "boolean" => ColumnTypeName::Bool,
28138 "date" => ColumnTypeName::Date,
28139 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
28140 "timestamptz" => ColumnTypeName::Timestamptz,
28141 "json" => ColumnTypeName::Json,
28142 "jsonb" => ColumnTypeName::Jsonb,
28143 "bytea" | "bytes" => ColumnTypeName::Bytes,
28144 "tsvector" => ColumnTypeName::TsVector,
28145 "tsquery" => ColumnTypeName::TsQuery,
28146 "uuid" => ColumnTypeName::Uuid,
28147 "interval" => ColumnTypeName::Interval,
28148 "time" => ColumnTypeName::Time,
28149 "year" => ColumnTypeName::Year,
28150 "timetz" => ColumnTypeName::TimeTz,
28151 "money" => ColumnTypeName::Money,
28152 _ => return None,
28153 })
28154}
28155
28156/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
28157/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
28158///
28159/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28160/// / embedded SQL land in v7.12.5+):
28161///
28162/// ```text
28163/// body := [ws] block [ws]
28164/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28165/// stmt := assign | return
28166/// assign := assign_target := expr
28167/// assign_target := ( NEW | OLD ) . ident | ident
28168/// return := RETURN ( NEW | OLD | NULL | expr )
28169/// ```
28170///
28171/// `expr` is parsed by recursing into the regular `Parser` — so a
28172/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28173/// NEW.subject || ' ' || NEW.sender)` body shape works without
28174/// the body parser knowing what `to_tsvector` is.
28175///
28176/// Errors here cause the caller to fall back to
28177/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28178/// successful, but the executor will refuse to invoke the
28179/// function with an "unparseable body" error.
28180/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28181/// from the crate root as `spg_sql::parse_function_body`.
28182pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28183 parse_plpgsql_body(body)
28184}
28185
28186fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28187 // Use the regular lexer on the body text. The trailing
28188 // `END;` may or may not have a semicolon; the lexer treats
28189 // both forms identically.
28190 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28191 message: alloc::format!("plpgsql body lex error: {e}"),
28192 token_pos: 0,
28193 })?;
28194 let mut parser = Parser::new(tokens);
28195 parser.parse_plpgsql_block()
28196}
28197
28198/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28199fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28200 match v {
28201 crate::ast::SetValue::String(s)
28202 | crate::ast::SetValue::Ident(s)
28203 | crate::ast::SetValue::Number(s) => s.clone(),
28204 crate::ast::SetValue::Default => "DEFAULT".into(),
28205 crate::ast::SetValue::Null => "NULL".into(),
28206 }
28207}
28208
28209/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28210/// contains an aggregate call at ITS OWN query level (recursion stops at
28211/// sublink boundaries — a sublink's aggregates belong to the sublink).
28212/// Backs the "aggregate functions are not allowed in a recursive query's
28213/// recursive term" well-formedness check.
28214/// v7.40.0 — is this the name of an aggregate? The same list
28215/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28216/// rewrite can leave an aggregate's ARGUMENT alone.
28217pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28218 AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28219}
28220
28221const AGG_NAMES: &[&str] = &[
28222 "count",
28223 "sum",
28224 "min",
28225 "max",
28226 "avg",
28227 "string_agg",
28228 "array_agg",
28229 "bool_and",
28230 "bool_or",
28231 "every",
28232 "any_value",
28233 "json_agg",
28234 "jsonb_agg",
28235 "json_object_agg",
28236 "jsonb_object_agg",
28237 "bit_and",
28238 "bit_or",
28239 "bit_xor",
28240 "var_pop",
28241 "var_samp",
28242 "variance",
28243 "std",
28244 "stddev",
28245 "stddev_pop",
28246 "stddev_samp",
28247 "range_agg",
28248 "range_intersect_agg",
28249 "percentile_cont",
28250 "percentile_disc",
28251 "mode",
28252 "corr",
28253 "covar_pop",
28254 "covar_samp",
28255];
28256
28257fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28258 match e {
28259 Expr::AggregateOrdered { .. } => true,
28260 Expr::FunctionCall { name, args } => {
28261 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28262 || args.iter().any(expr_has_toplevel_aggregate)
28263 }
28264 Expr::NamedArg { expr, .. }
28265 | Expr::Variadic(expr)
28266 | Expr::Unary { expr, .. }
28267 | Expr::Cast { expr, .. }
28268 | Expr::IsNull { expr, .. }
28269 | Expr::FieldAccess { base: expr, .. }
28270 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28271 Expr::Binary { lhs, rhs, .. } => {
28272 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28273 }
28274 Expr::Like { expr, pattern, .. } => {
28275 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28276 }
28277 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28278 Expr::InList { expr, list, .. } => {
28279 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28280 }
28281 Expr::ArraySubscript { target, index } => {
28282 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28283 }
28284 Expr::ArraySlice { target, lo, hi } => {
28285 expr_has_toplevel_aggregate(target)
28286 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28287 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28288 }
28289 Expr::AnyAll { expr, array, .. } => {
28290 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28291 }
28292 Expr::Case {
28293 operand,
28294 branches,
28295 else_branch,
28296 } => {
28297 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28298 || branches
28299 .iter()
28300 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28301 || else_branch
28302 .as_deref()
28303 .is_some_and(expr_has_toplevel_aggregate)
28304 }
28305 // The outer-level operands of a sublink can aggregate; the sublink's
28306 // own body cannot leak its aggregates up here.
28307 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28308 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28309 row.iter().any(expr_has_toplevel_aggregate)
28310 }
28311 _ => false,
28312 }
28313}
28314
28315/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28316/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28317/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28318/// sublink and is legal in a recursive term, so it is not walked here.
28319fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28320 let mut exprs: Vec<&Expr> = Vec::new();
28321 for it in &s.items {
28322 if let crate::ast::SelectItem::Expr { expr, .. } = it {
28323 exprs.push(expr);
28324 }
28325 }
28326 if let Some(w) = &s.where_ {
28327 exprs.push(w);
28328 }
28329 if let Some(h) = &s.having {
28330 exprs.push(h);
28331 }
28332 if let Some(g) = &s.group_by {
28333 exprs.extend(g.iter());
28334 }
28335 if let Some(from) = &s.from {
28336 for j in &from.joins {
28337 if let Some(on) = &j.on {
28338 exprs.push(on);
28339 }
28340 }
28341 }
28342 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28343}
28344
28345/// Does this expression contain a sublink whose subquery mentions `name`?
28346fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28347 match e {
28348 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28349 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28350 Expr::InSubquery { expr, subquery, .. } => {
28351 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28352 }
28353 Expr::RowInSubquery { row, subquery, .. } => {
28354 row.iter().any(|x| expr_sublink_mentions(x, name))
28355 || select_mentions_table(subquery, name)
28356 }
28357 Expr::RowCmpSubquery { row, subquery, .. } => {
28358 row.iter().any(|x| expr_sublink_mentions(x, name))
28359 || select_mentions_table(subquery, name)
28360 }
28361 Expr::NamedArg { expr, .. }
28362 | Expr::Variadic(expr)
28363 | Expr::Unary { expr, .. }
28364 | Expr::Cast { expr, .. }
28365 | Expr::IsNull { expr, .. }
28366 | Expr::FieldAccess { base: expr, .. }
28367 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28368 Expr::Binary { lhs, rhs, .. } => {
28369 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28370 }
28371 Expr::Like { expr, pattern, .. } => {
28372 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28373 }
28374 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28375 args.iter().any(|x| expr_sublink_mentions(x, name))
28376 }
28377 Expr::InList { expr, list, .. } => {
28378 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28379 }
28380 Expr::ArraySubscript { target, index } => {
28381 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28382 }
28383 Expr::ArraySlice { target, lo, hi } => {
28384 expr_sublink_mentions(target, name)
28385 || lo
28386 .as_deref()
28387 .is_some_and(|x| expr_sublink_mentions(x, name))
28388 || hi
28389 .as_deref()
28390 .is_some_and(|x| expr_sublink_mentions(x, name))
28391 }
28392 Expr::AnyAll { expr, array, .. } => {
28393 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28394 }
28395 Expr::Case {
28396 operand,
28397 branches,
28398 else_branch,
28399 } => {
28400 operand
28401 .as_deref()
28402 .is_some_and(|x| expr_sublink_mentions(x, name))
28403 || branches
28404 .iter()
28405 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28406 || else_branch
28407 .as_deref()
28408 .is_some_and(|x| expr_sublink_mentions(x, name))
28409 }
28410 _ => false,
28411 }
28412}
28413
28414/// Does this SELECT (in full — FROM tables, derived tables, its own
28415/// sublinks, and union arms) mention the named table?
28416fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28417 if let Some(from) = &s.from {
28418 if from.primary.name.eq_ignore_ascii_case(name) {
28419 return true;
28420 }
28421 if let Some(sub) = &from.primary.lateral_subquery
28422 && select_mentions_table(sub, name)
28423 {
28424 return true;
28425 }
28426 for j in &from.joins {
28427 if j.table.name.eq_ignore_ascii_case(name) {
28428 return true;
28429 }
28430 if let Some(sub) = &j.table.lateral_subquery
28431 && select_mentions_table(sub, name)
28432 {
28433 return true;
28434 }
28435 }
28436 }
28437 if select_has_self_ref_in_sublink(s, name) {
28438 return true;
28439 }
28440 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28441}
28442
28443/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28444/// row count, the way PG evaluates one before applying it.
28445///
28446/// `None` = not a constant (a column, a subquery, a function call).
28447/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28448/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28449/// All wordings were read off live PG 18.4.
28450fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28451 use crate::ast::{BinOp, Expr, Literal, UnOp};
28452 match e {
28453 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28454 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28455 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28456 }
28457 // PG coerces a string by its CONTENT, and fails on the value.
28458 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28459 |_| {
28460 Err(alloc::format!(
28461 "invalid input syntax for type bigint: \"{t}\""
28462 ))
28463 },
28464 |n| Ok(i128::from(n)),
28465 )),
28466 Expr::Literal(Literal::Bool(_)) => Some(Err(
28467 "argument of {L} must be type bigint, not type boolean".into(),
28468 )),
28469 Expr::Unary {
28470 op: UnOp::Neg,
28471 expr,
28472 } => match fold_limit_constant(expr)? {
28473 Ok(v) => Some(Ok(-v)),
28474 e @ Err(_) => Some(e),
28475 },
28476 Expr::Binary { lhs, op, rhs } => {
28477 let a = match fold_limit_constant(lhs)? {
28478 Ok(v) => v,
28479 e @ Err(_) => return Some(e),
28480 };
28481 let b = match fold_limit_constant(rhs)? {
28482 Ok(v) => v,
28483 e @ Err(_) => return Some(e),
28484 };
28485 let out = match op {
28486 BinOp::Add => a.checked_add(b),
28487 BinOp::Sub => a.checked_sub(b),
28488 BinOp::Mul => a.checked_mul(b),
28489 BinOp::Div if b != 0 => a.checked_div(b),
28490 BinOp::Div => return Some(Err("division by zero".into())),
28491 BinOp::Mod if b != 0 => a.checked_rem(b),
28492 BinOp::Mod => return Some(Err("division by zero".into())),
28493 _ => return None,
28494 };
28495 // PG evaluates the arithmetic in the operand's own type, so an
28496 // int-by-int product that leaves int range fails there — before
28497 // the row count is ever looked at.
28498 match out {
28499 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28500 Some(Err("integer out of range".into()))
28501 }
28502 Some(v) => Some(Ok(v)),
28503 None => Some(Err("integer out of range".into())),
28504 }
28505 }
28506 _ => None,
28507 }
28508}
28509
28510/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28511/// cast, which is what makes `LIMIT 2.5` keep three rows.
28512fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28513 if scale == 0 {
28514 return unscaled;
28515 }
28516 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28517 return 0;
28518 };
28519 let neg = unscaled < 0;
28520 let mag = unscaled.unsigned_abs() as i128;
28521 let rounded = (mag + div / 2) / div;
28522 if neg { -rounded } else { rounded }
28523}
28524
28525#[cfg(test)]
28526mod tests {
28527 use super::*;
28528 use alloc::string::ToString;
28529
28530 fn parse(s: &str) -> Statement {
28531 parse_statement(s).expect("parse ok")
28532 }
28533
28534 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28535 // `tables`, `partition`, etc. are unreserved keywords per PG's
28536 // `pg_get_keywords()` and MUST be usable as column / table /
28537 // alias names. Pre-T4 every drop-in user whose schema had one
28538 // of these as a column name (sentori events.release, mailrs
28539 // messages.index in some forks) blew the parser up at CREATE
28540 // TABLE time with "expected identifier, got Release". The
28541 // generalisation lives in `unreserved_keyword_text` + the
28542 // `expect_ident_like` and `parse_atom` arms that consult it.
28543 #[test]
28544 fn release_usable_as_column_name_in_create_table() {
28545 let stmt =
28546 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28547 if let Statement::CreateTable(t) = stmt {
28548 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28549 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28550 } else {
28551 panic!("expected CreateTable");
28552 }
28553 }
28554
28555 #[test]
28556 fn release_usable_as_column_ref_in_select_projection() {
28557 // The sentori `0003_partition_events.sql` INSERT-SELECT
28558 // walk references `release` in both column lists; the
28559 // projection-side use exercises `parse_atom`'s relaxed
28560 // identifier set.
28561 parse("SELECT id, release, payload FROM events WHERE id = 1");
28562 }
28563
28564 #[test]
28565 fn release_usable_as_column_ref_in_insert_column_list() {
28566 // INSERT INTO t (id, release, payload) VALUES (…)
28567 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28568 }
28569
28570 #[test]
28571 fn alter_column_drop_not_null_uses_keyword_drop_token() {
28572 // Sentori `0013_audit_tombstone.sql` issues
28573 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28574 // emits Token::Drop (not Ident("drop")); the parser must
28575 // accept both in the ALTER COLUMN sub-dispatch.
28576 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28577 }
28578
28579 #[test]
28580 fn create_index_accepts_parenthesised_expression_key() {
28581 // sentori `0040_events_bundle_idx.sql` shape — JSONB
28582 // expression index. Pre-T4 the parser bailed at the
28583 // inner `(` with "expected column ident or expression,
28584 // got LParen". The Token::LParen arm in CREATE INDEX
28585 // routes through the expression parser instead.
28586 parse(
28587 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28588 ON events ((payload->'bundle'->>'id'))",
28589 );
28590 }
28591
28592 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28593 // surface as parse errors, never stack overflows (embed hosts
28594 // abort on overflow).
28595 /// The nesting budget is a COUNT; what it has to fit inside is a
28596 /// number of BYTES, and only one of those two is stable across
28597 /// compiler versions. Round 847 measured 30,336 bytes per level
28598 /// after a toolchain move, which puts 64 levels at 1.94 MB and
28599 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28600 /// aborted instead of erroring, which is precisely the outcome it
28601 /// exists to rule out.
28602 ///
28603 /// So the budget is metered rather than assumed. The ceiling leaves
28604 /// the depth SPG advertises fitting in a default 2 MiB thread with
28605 /// room to spare, in the debug build, where frames are widest.
28606 #[test]
28607 fn nesting_frame_cost_stays_under_ceiling() {
28608 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28609 // thread keeps a margin for whatever called the parser.
28610 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28611
28612 frame_meter::reset();
28613 let depth = frame_meter::SAMPLE_HI + 8;
28614 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28615 parse(&sql);
28616
28617 let per_level = frame_meter::bytes_per_level();
28618 {
28619 extern crate std;
28620 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28621 }
28622 assert!(
28623 per_level <= CEILING,
28624 "{per_level} bytes per nesting level exceeds {CEILING}; \
28625 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28626 in parse_expr_inner / parse_unary rather than lowering the \
28627 depth or widening the stack.",
28628 per_level * MAX_NEST_DEPTH
28629 );
28630 }
28631
28632 #[test]
28633 fn nesting_budget_errors_cleanly() {
28634 let depth = MAX_NEST_DEPTH + 50;
28635 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28636 let err = parse_statement(&sql).expect_err("must reject");
28637 assert!(err.message.contains("nests deeper"), "{err:?}");
28638 // Within budget still parses.
28639 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28640 parse(&sql);
28641 }
28642
28643 #[test]
28644 fn binary_chain_budget_errors_cleanly() {
28645 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28646 let err = parse_statement(&sql).expect_err("must reject");
28647 assert!(err.message.contains("chained binary"), "{err:?}");
28648 // Within budget still parses (chain depth ≤ budget is safe
28649 // for recursive eval/drop on 2 MiB stacks).
28650 let sql = format!("SELECT 1{}", " + 1".repeat(200));
28651 parse(&sql);
28652 }
28653
28654 #[test]
28655 fn in_list_unaffected_by_chain_budget() {
28656 // Flat InList: 20k elements parse fine and stay flat.
28657 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28658 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28659 let Statement::Select(s) = parse(&sql) else {
28660 panic!("expected select")
28661 };
28662 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28663 panic!("expected flat InList, got {:?}", s.where_)
28664 };
28665 assert_eq!(list.len(), 20_000);
28666 assert!(!negated);
28667 }
28668
28669 fn lit_int(n: i64) -> Expr {
28670 Expr::Literal(Literal::Integer(n))
28671 }
28672
28673 fn col(name: &str) -> Expr {
28674 Expr::Column(ColumnName {
28675 qualifier: None,
28676 name: name.into(),
28677 })
28678 }
28679
28680 #[test]
28681 fn select_single_integer() {
28682 let s = parse("SELECT 1");
28683 let Statement::Select(s) = s else {
28684 panic!("expected SELECT")
28685 };
28686 assert_eq!(s.items.len(), 1);
28687 assert!(s.from.is_none());
28688 assert!(s.where_.is_none());
28689 }
28690
28691 #[test]
28692 fn select_multiple_literal_kinds() {
28693 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28694 let Statement::Select(s) = s else {
28695 panic!("expected SELECT")
28696 };
28697 assert_eq!(s.items.len(), 5);
28698 }
28699
28700 #[test]
28701 fn select_wildcard_from_table() {
28702 let s = parse("SELECT * FROM users");
28703 let Statement::Select(s) = s else {
28704 panic!("expected SELECT")
28705 };
28706 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28707 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28708 }
28709
28710 #[test]
28711 fn select_with_table_alias() {
28712 let s = parse("SELECT * FROM users AS u");
28713 let Statement::Select(s) = s else {
28714 panic!("expected SELECT")
28715 };
28716 let t = &s.from.as_ref().unwrap().primary;
28717 assert_eq!(t.name, "users");
28718 assert_eq!(t.alias.as_deref(), Some("u"));
28719 }
28720
28721 #[test]
28722 fn select_with_where_eq() {
28723 let s = parse("SELECT a FROM t WHERE a = 1");
28724 let Statement::Select(s) = s else {
28725 panic!("expected SELECT")
28726 };
28727 let w = s.where_.unwrap();
28728 assert_eq!(
28729 w,
28730 Expr::Binary {
28731 lhs: Box::new(col("a")),
28732 op: BinOp::Eq,
28733 rhs: Box::new(lit_int(1)),
28734 }
28735 );
28736 }
28737
28738 #[test]
28739 fn arithmetic_precedence() {
28740 let s = parse("SELECT 1 + 2 * 3");
28741 let Statement::Select(s) = s else {
28742 panic!("expected SELECT")
28743 };
28744 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28745 panic!("wildcard?")
28746 };
28747 assert_eq!(
28748 expr,
28749 &Expr::Binary {
28750 lhs: Box::new(lit_int(1)),
28751 op: BinOp::Add,
28752 rhs: Box::new(Expr::Binary {
28753 lhs: Box::new(lit_int(2)),
28754 op: BinOp::Mul,
28755 rhs: Box::new(lit_int(3)),
28756 }),
28757 }
28758 );
28759 }
28760
28761 #[test]
28762 fn parentheses_override_precedence() {
28763 let s = parse("SELECT (1 + 2) * 3");
28764 let Statement::Select(s) = s else {
28765 panic!("expected SELECT")
28766 };
28767 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28768 panic!()
28769 };
28770 assert_eq!(
28771 expr,
28772 &Expr::Binary {
28773 lhs: Box::new(Expr::Binary {
28774 lhs: Box::new(lit_int(1)),
28775 op: BinOp::Add,
28776 rhs: Box::new(lit_int(2)),
28777 }),
28778 op: BinOp::Mul,
28779 rhs: Box::new(lit_int(3)),
28780 }
28781 );
28782 }
28783
28784 #[test]
28785 fn not_binds_below_comparison() {
28786 // `NOT a = 1` should parse as `NOT (a = 1)`.
28787 let s = parse("SELECT NOT a = 1 FROM t");
28788 let Statement::Select(s) = s else {
28789 panic!("expected SELECT")
28790 };
28791 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28792 panic!()
28793 };
28794 assert_eq!(
28795 expr,
28796 &Expr::Unary {
28797 op: UnOp::Not,
28798 expr: Box::new(Expr::Binary {
28799 lhs: Box::new(col("a")),
28800 op: BinOp::Eq,
28801 rhs: Box::new(lit_int(1)),
28802 }),
28803 }
28804 );
28805 }
28806
28807 #[test]
28808 fn unary_minus_binds_above_multiplication() {
28809 // `-a * 2` should be `(-a) * 2`.
28810 let s = parse("SELECT -a * 2 FROM t");
28811 let Statement::Select(s) = s else {
28812 panic!("expected SELECT")
28813 };
28814 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28815 panic!()
28816 };
28817 assert_eq!(
28818 expr,
28819 &Expr::Binary {
28820 lhs: Box::new(Expr::Unary {
28821 op: UnOp::Neg,
28822 expr: Box::new(col("a")),
28823 }),
28824 op: BinOp::Mul,
28825 rhs: Box::new(lit_int(2)),
28826 }
28827 );
28828 }
28829
28830 #[test]
28831 fn qualified_column() {
28832 let s = parse("SELECT t.col FROM t");
28833 let Statement::Select(s) = s else {
28834 panic!("expected SELECT")
28835 };
28836 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28837 panic!()
28838 };
28839 assert_eq!(
28840 expr,
28841 &Expr::Column(ColumnName {
28842 qualifier: Some("t".into()),
28843 name: "col".into()
28844 })
28845 );
28846 }
28847
28848 #[test]
28849 fn select_item_alias_with_as() {
28850 let s = parse("SELECT a AS y FROM t");
28851 let Statement::Select(s) = s else {
28852 panic!("expected SELECT")
28853 };
28854 let SelectItem::Expr { alias, .. } = &s.items[0] else {
28855 panic!()
28856 };
28857 assert_eq!(alias.as_deref(), Some("y"));
28858 }
28859
28860 #[test]
28861 fn trailing_semicolon_accepted() {
28862 let s = parse("SELECT 1;");
28863 let Statement::Select(s) = s else {
28864 panic!("expected SELECT")
28865 };
28866 assert_eq!(s.items.len(), 1);
28867 }
28868
28869 #[test]
28870 fn boolean_chain_with_and_or_not() {
28871 // (NOT a) OR (b AND (NOT c))
28872 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28873 let Statement::Select(s) = s else {
28874 panic!("expected SELECT")
28875 };
28876 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28877 panic!()
28878 };
28879 let expected = Expr::Binary {
28880 lhs: Box::new(Expr::Unary {
28881 op: UnOp::Not,
28882 expr: Box::new(col("a")),
28883 }),
28884 op: BinOp::Or,
28885 rhs: Box::new(Expr::Binary {
28886 lhs: Box::new(col("b")),
28887 op: BinOp::And,
28888 rhs: Box::new(Expr::Unary {
28889 op: UnOp::Not,
28890 expr: Box::new(col("c")),
28891 }),
28892 }),
28893 };
28894 assert_eq!(expr, &expected);
28895 }
28896
28897 #[test]
28898 fn empty_input_errors() {
28899 // v7.14.0 — pg_dump preambles emit several comment-only
28900 // / blank-line statements that collapse to Statement::
28901 // Empty rather than a parse error. The old "SELECT in
28902 // message" assertion is stale; verify the new contract:
28903 // empty / whitespace / comment-only input parses to
28904 // Statement::Empty.
28905 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28906 assert!(matches!(
28907 parse_statement(" \n\t ").unwrap(),
28908 Statement::Empty
28909 ));
28910 // Sanity: malformed-but-non-empty still errors.
28911 assert!(parse_statement("SELECT FROM WHERE").is_err());
28912 }
28913
28914 #[test]
28915 fn unmatched_paren_errors() {
28916 assert!(parse_statement("SELECT (1 + 2").is_err());
28917 }
28918
28919 #[test]
28920 fn display_round_trip_simple_select() {
28921 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28922 let text = original.to_string();
28923 let again = parse_statement(&text).expect("re-parse");
28924 assert_eq!(original, again);
28925 }
28926
28927 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28928
28929 #[test]
28930 fn create_table_single_column() {
28931 let s = parse("CREATE TABLE foo (a INT)");
28932 let Statement::CreateTable(c) = s else {
28933 panic!("expected CreateTable")
28934 };
28935 assert_eq!(c.name, "foo");
28936 assert_eq!(c.columns.len(), 1);
28937 assert_eq!(c.columns[0].name, "a");
28938 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28939 assert!(c.columns[0].nullable);
28940 }
28941
28942 #[test]
28943 fn create_table_multi_column_with_not_null_mix() {
28944 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28945 let Statement::CreateTable(c) = s else {
28946 panic!()
28947 };
28948 assert_eq!(c.columns.len(), 4);
28949 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28950 assert!(!c.columns[0].nullable);
28951 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28952 assert!(c.columns[1].nullable);
28953 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28954 assert!(!c.columns[2].nullable);
28955 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28956 }
28957
28958 #[test]
28959 fn create_table_bigint_supported() {
28960 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28961 let Statement::CreateTable(c) = s else {
28962 panic!()
28963 };
28964 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28965 }
28966
28967 #[test]
28968 fn create_table_vector_default_is_f32() {
28969 let s = parse("CREATE TABLE t (v VECTOR(128))");
28970 let Statement::CreateTable(c) = s else {
28971 panic!()
28972 };
28973 assert_eq!(
28974 c.columns[0].ty,
28975 ColumnTypeName::Vector {
28976 dim: 128,
28977 encoding: VecEncoding::F32,
28978 },
28979 );
28980 }
28981
28982 #[test]
28983 fn create_table_vector_using_sq8() {
28984 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28985 // Case-insensitive on both `USING` and the encoding name.
28986 for sql in [
28987 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28988 "CREATE TABLE t (v VECTOR(128) using sq8)",
28989 ] {
28990 let s = parse(sql);
28991 let Statement::CreateTable(c) = s else {
28992 panic!()
28993 };
28994 assert_eq!(
28995 c.columns[0].ty,
28996 ColumnTypeName::Vector {
28997 dim: 128,
28998 encoding: VecEncoding::Sq8,
28999 },
29000 "{sql}",
29001 );
29002 }
29003 }
29004
29005 #[test]
29006 fn create_table_vector_using_unknown_errors() {
29007 // v7.16.1 — the inline `USING <encoding>` shape on
29008 // CREATE TABLE column defs was withdrawn before
29009 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
29010 // (col vector_<metric>_ops)`; the parser now rejects
29011 // USING at column-list position with a clearer
29012 // "expected ',' or ')'" message. Test asserts the
29013 // current rejection, not the old "unknown vector
29014 // encoding" string.
29015 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
29016 assert!(
29017 err.message.contains("USING")
29018 || err.message.contains("using")
29019 || err.message.contains("')'")
29020 || err.message.contains("','"),
29021 "expected USING/column-list rejection, got: {}",
29022 err.message
29023 );
29024 }
29025
29026 #[test]
29027 fn vector_using_sq8_display_roundtrips() {
29028 // The Display impl must produce text that re-parses to the
29029 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
29030 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
29031 let Statement::CreateTable(c) = s else {
29032 panic!()
29033 };
29034 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
29035 }
29036
29037 #[test]
29038 fn parser_recognises_placeholders() {
29039 use crate::ast::{Expr, SelectItem, Statement};
29040 // $N in expression position parses as Expr::Placeholder(N).
29041 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
29042 let Statement::Select(sel) = s else { panic!() };
29043 assert!(matches!(
29044 sel.items[0],
29045 SelectItem::Expr {
29046 expr: Expr::Placeholder(1),
29047 alias: None
29048 }
29049 ));
29050 // $2 + 1
29051 let SelectItem::Expr {
29052 expr: Expr::Binary { lhs, rhs, .. },
29053 ..
29054 } = &sel.items[1]
29055 else {
29056 panic!()
29057 };
29058 assert!(matches!(**lhs, Expr::Placeholder(2)));
29059 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
29060 // WHERE x = $3
29061 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
29062 panic!()
29063 };
29064 assert!(matches!(**rhs, Expr::Placeholder(3)));
29065 }
29066
29067 #[test]
29068 fn parser_rejects_dollar_zero() {
29069 // $0 is not valid in PG; the lexer rejects it.
29070 assert!(parse_statement("SELECT $0").is_err());
29071 }
29072
29073 #[test]
29074 fn placeholder_display_roundtrips() {
29075 // The Display impl must produce text that re-lexes to the
29076 // same Placeholder token.
29077 let s = parse("SELECT $42 FROM t");
29078 let printed = s.to_string();
29079 assert!(printed.contains("$42"));
29080 let again = parse(&printed);
29081 assert_eq!(s, again);
29082 }
29083
29084 #[test]
29085 fn alter_index_rebuild_bare() {
29086 use crate::ast::{AlterIndexTarget, Statement};
29087 let s = parse("ALTER INDEX my_idx REBUILD");
29088 let Statement::AlterIndex(a) = s else {
29089 panic!("expected AlterIndex, got {s:?}")
29090 };
29091 assert_eq!(a.name, "my_idx");
29092 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
29093 }
29094
29095 #[test]
29096 fn alter_index_rebuild_with_encoding() {
29097 use crate::ast::{AlterIndexTarget, Statement};
29098 for (sql, want) in [
29099 (
29100 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
29101 VecEncoding::F32,
29102 ),
29103 (
29104 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
29105 VecEncoding::Sq8,
29106 ),
29107 (
29108 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29109 VecEncoding::F16,
29110 ),
29111 ] {
29112 let s = parse(sql);
29113 let Statement::AlterIndex(a) = s else {
29114 panic!("{sql}: expected AlterIndex")
29115 };
29116 assert_eq!(a.name, "my_idx");
29117 assert_eq!(
29118 a.target,
29119 AlterIndexTarget::Rebuild {
29120 encoding: Some(want)
29121 },
29122 "{sql}"
29123 );
29124 }
29125 }
29126
29127 #[test]
29128 fn alter_index_rebuild_unknown_encoding_errors() {
29129 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
29130 assert!(
29131 err.message.contains("unknown vector encoding"),
29132 "got: {}",
29133 err.message
29134 );
29135 }
29136
29137 #[test]
29138 fn alter_index_rebuild_display_roundtrips() {
29139 for (input, want) in [
29140 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
29141 (
29142 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29143 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29144 ),
29145 (
29146 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29147 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29148 ),
29149 ] {
29150 let s = parse(input);
29151 assert_eq!(s.to_string(), want);
29152 }
29153 }
29154
29155 #[test]
29156 fn create_table_unknown_type_defers_to_engine() {
29157 // v4.9 picked XML as a parse-time "unsupported column
29158 // type" probe. v7.17.0 Phase 1.4 changed the contract:
29159 // an unknown type ident parses as Text + `user_type_ref`
29160 // so CREATE TABLE can resolve user-defined enum / domain
29161 // types — rejection of truly-unknown types moved to the
29162 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29163 // to a first-class built-in, so this probe switched to a
29164 // synthetic name nothing in the lexer will ever recognise.
29165 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29166 let Statement::CreateTable(t) = stmt else {
29167 panic!("expected CreateTable");
29168 };
29169 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29170 }
29171
29172 #[test]
29173 fn create_table_missing_table_keyword_errors() {
29174 assert!(parse_statement("CREATE x (a INT)").is_err());
29175 }
29176
29177 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29178 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29179
29180 #[test]
29181 fn parse_create_table_partition_by_range() {
29182 use crate::ast::{PartitionBySpec, PartitionKindAst};
29183 let stmt = parse_statement(
29184 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29185 payload JSONB) PARTITION BY RANGE (ts)",
29186 )
29187 .unwrap();
29188 let Statement::CreateTable(t) = stmt else {
29189 panic!("expected CreateTable");
29190 };
29191 assert!(t.partition_of.is_none(), "parent has no partition_of");
29192 assert_eq!(t.columns.len(), 3);
29193 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29194 assert_eq!(
29195 by,
29196 &PartitionBySpec {
29197 kind: PartitionKindAst::Range,
29198 key_columns: alloc::vec!["ts".to_string()],
29199 }
29200 );
29201 // Display round-trip preserves the suffix. `quote_ident`
29202 // only adds double quotes when the ident needs escaping, so
29203 // a plain `ts` survives bare here.
29204 assert!(
29205 t.to_string().contains("PARTITION BY RANGE (ts)"),
29206 "Display lost PARTITION BY suffix: {t}"
29207 );
29208 }
29209
29210 #[test]
29211 fn parse_create_table_partition_of_range() {
29212 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29213 let stmt = parse_statement(
29214 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29215 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29216 )
29217 .unwrap();
29218 let Statement::CreateTable(t) = stmt else {
29219 panic!("expected CreateTable");
29220 };
29221 assert!(t.columns.is_empty(), "child inherits columns from parent");
29222 assert!(t.partition_by.is_none());
29223 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29224 assert_eq!(of.parent_name, "events_partitioned");
29225 let PartitionOfSpec { bounds, .. } = of.clone();
29226 match bounds {
29227 PartitionOfBoundsAst::Range { lower, upper } => {
29228 assert!(lower.to_string().contains("2026-06-01"));
29229 assert!(upper.to_string().contains("2026-07-01"));
29230 }
29231 other => panic!("expected Range, got {other:?}"),
29232 }
29233 // Display round-trip emits the FOR VALUES tail. `quote_ident`
29234 // skips quotes when not required, so the parent name appears
29235 // bare here.
29236 let s = t.to_string();
29237 assert!(
29238 s.contains("PARTITION OF events_partitioned"),
29239 "Display lost PARTITION OF: {s}"
29240 );
29241 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29242 assert!(s.contains(") TO ("), "Display lost TO: {s}");
29243 }
29244
29245 #[test]
29246 fn parse_create_table_partition_of_default() {
29247 use crate::ast::PartitionOfBoundsAst;
29248 let stmt =
29249 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29250 .unwrap();
29251 let Statement::CreateTable(t) = stmt else {
29252 panic!("expected CreateTable");
29253 };
29254 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29255 assert_eq!(of.parent_name, "events_partitioned");
29256 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29257 assert!(
29258 t.to_string()
29259 .contains("PARTITION OF events_partitioned DEFAULT"),
29260 "Display lost DEFAULT: {t}"
29261 );
29262 }
29263
29264 #[test]
29265 fn parse_create_table_partition_by_list() {
29266 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29267 // child with `FOR VALUES IN (lit, lit, …)`.
29268 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29269 let parent =
29270 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29271 .unwrap();
29272 let Statement::CreateTable(t) = parent else {
29273 panic!("expected CreateTable");
29274 };
29275 let Some(PartitionBySpec {
29276 kind,
29277 ref key_columns,
29278 }) = t.partition_by
29279 else {
29280 panic!("expected PARTITION BY");
29281 };
29282 assert_eq!(kind, PartitionKindAst::List);
29283 assert_eq!(*key_columns, vec!["region".to_string()]);
29284 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29285
29286 let child = parse_statement(
29287 "CREATE TABLE events_apac PARTITION OF events_listed \
29288 FOR VALUES IN ('jp', 'kr', 'tw')",
29289 )
29290 .unwrap();
29291 let Statement::CreateTable(c) = child else {
29292 panic!("expected CreateTable");
29293 };
29294 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29295 let PartitionOfBoundsAst::List { values } = &of.bounds else {
29296 panic!("expected List bounds, got {:?}", of.bounds);
29297 };
29298 assert_eq!(values.len(), 3);
29299 let disp = c.to_string();
29300 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29301 }
29302
29303 #[test]
29304 fn parse_create_table_partition_by_hash() {
29305 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29306 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29307 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29308 let parent =
29309 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29310 let Statement::CreateTable(t) = parent else {
29311 panic!("expected CreateTable");
29312 };
29313 let Some(PartitionBySpec {
29314 kind,
29315 ref key_columns,
29316 }) = t.partition_by
29317 else {
29318 panic!("expected PARTITION BY");
29319 };
29320 assert_eq!(kind, PartitionKindAst::Hash);
29321 assert_eq!(*key_columns, vec!["id".to_string()]);
29322 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29323
29324 let child = parse_statement(
29325 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29326 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29327 )
29328 .unwrap();
29329 let Statement::CreateTable(c) = child else {
29330 panic!("expected CreateTable");
29331 };
29332 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29333 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29334 panic!("expected Hash bounds");
29335 };
29336 assert_eq!(modulus, 4);
29337 assert_eq!(remainder, 0);
29338 let disp = c.to_string();
29339 assert!(
29340 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29341 "Display lost HASH bounds: {disp}"
29342 );
29343
29344 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29345 let bad = parse_statement(
29346 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29347 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29348 );
29349 let msg = format!("{}", bad.unwrap_err());
29350 assert!(
29351 msg.contains("REMAINDER") && msg.contains("MODULUS"),
29352 "expected REMAINDER/MODULUS validation error: {msg}"
29353 );
29354 }
29355
29356 #[test]
29357 fn parse_create_table_partition_of_rejects_columns() {
29358 // v7.37.6-B contract: PARTITION OF children inherit columns
29359 // from the parent; an explicit list MUST surface as a parse
29360 // error rather than getting silently ignored.
29361 let err = parse_statement(
29362 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29363 FOR VALUES FROM ('a') TO ('b')",
29364 );
29365 assert!(err.is_err(), "expected parse error for explicit columns");
29366 let msg = format!("{}", err.unwrap_err());
29367 assert!(
29368 msg.contains("PARTITION OF") && msg.contains("column"),
29369 "error should mention PARTITION OF + columns: {msg}"
29370 );
29371 }
29372
29373 #[test]
29374 fn insert_single_value() {
29375 let s = parse("INSERT INTO foo VALUES (42)");
29376 let Statement::Insert(i) = s else {
29377 panic!("expected Insert")
29378 };
29379 assert_eq!(i.table, "foo");
29380 assert_eq!(i.rows.len(), 1);
29381 assert_eq!(i.rows[0].len(), 1);
29382 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29383 }
29384
29385 #[test]
29386 fn insert_multi_value_with_mixed_literals() {
29387 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29388 let Statement::Insert(i) = s else { panic!() };
29389 assert_eq!(i.rows.len(), 1);
29390 assert_eq!(i.rows[0].len(), 5);
29391 }
29392
29393 #[test]
29394 fn insert_missing_into_errors() {
29395 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29396 }
29397
29398 #[test]
29399 fn create_table_round_trip() {
29400 let original =
29401 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29402 let text = original.to_string();
29403 let again = parse_statement(&text).expect("re-parse");
29404 assert_eq!(original, again);
29405 }
29406
29407 #[test]
29408 fn insert_round_trip_with_negation_and_string() {
29409 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29410 let text = original.to_string();
29411 let again = parse_statement(&text).expect("re-parse");
29412 assert_eq!(original, again);
29413 }
29414
29415 #[test]
29416 fn unknown_keyword_at_statement_start_errors() {
29417 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29418 // the top-level dispatch still has no branch to take.
29419 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29420 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29421 }
29422
29423 // --- v0.8 CREATE INDEX --------------------------------------------------
29424
29425 #[test]
29426 fn create_index_basic() {
29427 let s = parse("CREATE INDEX idx_id ON users (id)");
29428 let Statement::CreateIndex(c) = s else {
29429 panic!("expected CreateIndex")
29430 };
29431 assert_eq!(c.name, "idx_id");
29432 assert_eq!(c.table, "users");
29433 assert_eq!(c.column, "id");
29434 }
29435
29436 #[test]
29437 fn create_index_missing_on_errors() {
29438 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29439 }
29440
29441 #[test]
29442 fn create_index_missing_paren_errors() {
29443 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29444 }
29445
29446 #[test]
29447 fn create_index_round_trip() {
29448 let original = parse("CREATE INDEX by_name ON users (name)");
29449 let again = parse_statement(&original.to_string()).unwrap();
29450 assert_eq!(original, again);
29451 }
29452
29453 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29454
29455 #[test]
29456 fn create_unique_index_basic() {
29457 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29458 let Statement::CreateIndex(c) = s else {
29459 panic!("expected CreateIndex");
29460 };
29461 assert!(c.is_unique);
29462 assert_eq!(c.column, "a");
29463 assert!(c.partial_predicate.is_none());
29464 }
29465
29466 #[test]
29467 fn create_unique_index_partial() {
29468 // mailrs's email_templates "one default per user" shape.
29469 let s = parse(
29470 "CREATE UNIQUE INDEX idx_email_templates_user_default \
29471 ON email_templates (user_address) WHERE is_default = true",
29472 );
29473 let Statement::CreateIndex(c) = s else {
29474 panic!("expected CreateIndex");
29475 };
29476 assert!(c.is_unique);
29477 assert_eq!(c.table, "email_templates");
29478 assert_eq!(c.column, "user_address");
29479 assert!(c.partial_predicate.is_some());
29480 }
29481
29482 #[test]
29483 fn create_unique_index_composite_with_predicate() {
29484 // mailrs's calendar_events instance: composite columns.
29485 let s = parse(
29486 "CREATE UNIQUE INDEX uq_calendar_events_instance \
29487 ON calendar_events (calendar_id, uid, recurrence_id) \
29488 WHERE recurrence_id IS NOT NULL",
29489 );
29490 let Statement::CreateIndex(c) = s else {
29491 panic!("expected CreateIndex");
29492 };
29493 assert!(c.is_unique);
29494 assert_eq!(c.column, "calendar_id");
29495 assert_eq!(
29496 c.extra_columns,
29497 vec!["uid".to_string(), "recurrence_id".to_string()]
29498 );
29499 assert!(c.partial_predicate.is_some());
29500 }
29501
29502 #[test]
29503 fn create_unique_index_using_btree_ok() {
29504 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29505 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29506 }
29507
29508 #[test]
29509 fn create_unique_index_using_hnsw_rejected() {
29510 let err =
29511 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29512 assert!(err.message.contains("UNIQUE"), "{}", err.message);
29513 }
29514
29515 #[test]
29516 fn create_unique_index_round_trip() {
29517 let original = parse(
29518 "CREATE UNIQUE INDEX uq_calendar_events_master \
29519 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29520 );
29521 let again = parse_statement(&original.to_string()).unwrap();
29522 assert_eq!(original, again);
29523 }
29524
29525 #[test]
29526 fn create_unique_without_index_errors() {
29527 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29528 // v7.39 (round 340, V56) — PG 18.4, verbatim.
29529 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29530 }
29531
29532 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29533
29534 #[test]
29535 fn create_table_bytea_column() {
29536 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29537 let Statement::CreateTable(c) = s else {
29538 panic!("expected CreateTable");
29539 };
29540 assert_eq!(c.columns.len(), 2);
29541 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29542 assert!(!c.columns[1].nullable);
29543 }
29544
29545 #[test]
29546 fn create_table_bytes_alias_column() {
29547 let s = parse("CREATE TABLE t (blob BYTES)");
29548 let Statement::CreateTable(c) = s else {
29549 panic!("expected CreateTable");
29550 };
29551 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29552 }
29553
29554 #[test]
29555 fn bytea_round_trip_display() {
29556 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29557 let again = parse_statement(&original.to_string()).unwrap();
29558 assert_eq!(original, again);
29559 }
29560
29561 // --- v0.9 transactions -------------------------------------------------
29562
29563 #[test]
29564 fn begin_commit_rollback_parse_as_unit_variants() {
29565 let plain = crate::ast::TransactionModes::default();
29566 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29567 assert_eq!(parse("COMMIT"), Statement::Commit);
29568 // r1066 — PG synonyms pgbench's tpcb script relies on.
29569 assert_eq!(parse("END"), Statement::Commit);
29570 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29571 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29572 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29573 // Trailing semicolons accepted too.
29574 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29575 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29576 // statement (with or without the WORK/TRANSACTION noise word).
29577 assert_eq!(
29578 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29579 Statement::Begin(crate::ast::TransactionModes {
29580 isolation: Some(IsolationLevel::RepeatableRead),
29581 read_only: None,
29582 deferrable: None,
29583 })
29584 );
29585 assert_eq!(
29586 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29587 Statement::Begin(crate::ast::TransactionModes {
29588 isolation: Some(IsolationLevel::Serializable),
29589 read_only: None,
29590 deferrable: None,
29591 })
29592 );
29593 // v7.39 — this line used to read
29594 //
29595 // // A non-isolation mode keeps the session default (None).
29596 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29597 //
29598 // which pinned the defect rather than catching it: the READ ONLY
29599 // was thrown away, so the statement opened an ordinary read-write
29600 // transaction and every write inside it was accepted. The
29601 // isolation level is still absent here, because this statement
29602 // does not name one — that part was right.
29603 assert_eq!(
29604 parse("BEGIN READ ONLY"),
29605 Statement::Begin(crate::ast::TransactionModes {
29606 isolation: None,
29607 read_only: Some(true),
29608 deferrable: None,
29609 })
29610 );
29611 assert_eq!(
29612 parse("START TRANSACTION READ WRITE"),
29613 Statement::Begin(crate::ast::TransactionModes {
29614 isolation: None,
29615 read_only: Some(false),
29616 deferrable: None,
29617 })
29618 );
29619 assert_eq!(
29620 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29621 Statement::Begin(crate::ast::TransactionModes {
29622 isolation: Some(IsolationLevel::Serializable),
29623 read_only: Some(true),
29624 deferrable: None,
29625 })
29626 );
29627 // v7.40.12 — DEFERRABLE was consumed and dropped, the way READ
29628 // ONLY was before v7.39, and for the same reason nothing noticed:
29629 // the statement parsed, so the clause looked handled. Measured on
29630 // PG 18.6, `BEGIN DEFERRABLE; SHOW transaction_deferrable` -> on.
29631 assert_eq!(
29632 parse("BEGIN DEFERRABLE"),
29633 Statement::Begin(crate::ast::TransactionModes {
29634 isolation: None,
29635 read_only: None,
29636 deferrable: Some(true),
29637 })
29638 );
29639 assert_eq!(
29640 parse("BEGIN NOT DEFERRABLE"),
29641 Statement::Begin(crate::ast::TransactionModes {
29642 isolation: None,
29643 read_only: None,
29644 deferrable: Some(false),
29645 })
29646 );
29647 assert_eq!(
29648 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE"),
29649 Statement::Begin(crate::ast::TransactionModes {
29650 isolation: Some(IsolationLevel::Serializable),
29651 read_only: Some(true),
29652 deferrable: Some(true),
29653 })
29654 );
29655 }
29656
29657 /// v7.40.12 — PG's multi-word SHOW spellings. `SHOW TIME ZONE` was
29658 /// known only to a pgwire shortcut that answered SHOW from a copy;
29659 /// when that shortcut went, the spelling had to live here, where the
29660 /// embedded API gets it too. `SHOW SESSION AUTHORIZATION` was known
29661 /// nowhere: `session` is a bare ident, so the statement ended there
29662 /// and the next word was a syntax error.
29663 #[test]
29664 fn show_multi_word_spellings_parse() {
29665 assert_eq!(
29666 parse("SHOW TIME ZONE"),
29667 Statement::ShowParameter("timezone".to_string())
29668 );
29669 assert_eq!(
29670 parse("SHOW SESSION AUTHORIZATION"),
29671 Statement::ShowParameter("session_authorization".to_string())
29672 );
29673 assert_eq!(
29674 parse("SHOW TRANSACTION ISOLATION LEVEL"),
29675 Statement::ShowParameter("transaction_isolation".to_string())
29676 );
29677 // The single-word spellings still reach the same handler.
29678 assert_eq!(
29679 parse("SHOW timezone"),
29680 Statement::ShowParameter("timezone".to_string())
29681 );
29682 assert_eq!(
29683 parse("SHOW session_authorization"),
29684 Statement::ShowParameter("session_authorization".to_string())
29685 );
29686 }
29687
29688 // --- v1.2: pgvector distance ops + ::vector cast --------------------
29689
29690 #[test]
29691 fn inner_product_binop_parses() {
29692 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29693 let Statement::Select(s) = s else { panic!() };
29694 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29695 panic!()
29696 };
29697 assert!(matches!(
29698 expr,
29699 Expr::Binary {
29700 op: BinOp::InnerProduct,
29701 ..
29702 }
29703 ));
29704 }
29705
29706 #[test]
29707 fn cosine_distance_binop_parses() {
29708 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29709 let Statement::Select(s) = s else { panic!() };
29710 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29711 panic!()
29712 };
29713 assert!(matches!(
29714 expr,
29715 Expr::Binary {
29716 op: BinOp::CosineDistance,
29717 ..
29718 }
29719 ));
29720 }
29721
29722 #[test]
29723 fn vector_cast_postfix_wraps_string_literal() {
29724 let s = parse("SELECT '[1,2,3]'::vector FROM t");
29725 let Statement::Select(s) = s else { panic!() };
29726 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29727 panic!()
29728 };
29729 assert!(matches!(
29730 expr,
29731 Expr::Cast {
29732 target: CastTarget::Vector,
29733 ..
29734 }
29735 ));
29736 }
29737
29738 #[test]
29739 fn unsupported_cast_target_errors() {
29740 // v7.37.5 ship triage promoted the parser to accept every
29741 // ident as a `CastTarget::Named(canonical)`; the engine
29742 // surfaces the "unsupported cast target" error at eval
29743 // time when `type_name_to_data_type` can't resolve it.
29744 // Parser-side error now requires a NON-ident after `::`
29745 // (e.g. a punctuation token).
29746 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29747 assert_eq!(err.message, "syntax error at or near \",\"");
29748 }
29749
29750 #[test]
29751 fn tx_statements_round_trip() {
29752 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29753 let original = parse(q);
29754 let again = parse_statement(&original.to_string()).unwrap();
29755 assert_eq!(original, again);
29756 }
29757 }
29758
29759 #[test]
29760 fn interval_text_parsing_units() {
29761 // v7.37.5 β — three-field shape `(months, days, micros)` so
29762 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29763 // Single unit.
29764 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29765 assert_eq!(
29766 parse_interval_text("24 hours"),
29767 Some((0, 0, 86_400_000_000))
29768 );
29769 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29770 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29771 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29772 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29773 // Compound spans accumulate per-dimension.
29774 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29775 assert_eq!(
29776 parse_interval_text("1 day 2 hours"),
29777 Some((0, 1, 7_200_000_000))
29778 );
29779 // Negative numbers carry through per-dimension.
29780 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29781 // Bad shapes return None.
29782 assert_eq!(parse_interval_text(""), None);
29783 assert_eq!(parse_interval_text("garbage"), None);
29784 assert_eq!(parse_interval_text("1 fortnight"), None);
29785 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29786 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29787 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29788 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29789 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29790 }
29791
29792 #[test]
29793 fn interval_literal_roundtrips_via_display() {
29794 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29795 let s = parsed.to_string();
29796 // Display preserves the original text verbatim.
29797 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29798 // And re-parsing yields a structurally equal statement.
29799 let again = parse_statement(&s).unwrap();
29800 assert_eq!(parsed, again);
29801 }
29802
29803 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29804
29805 #[test]
29806 fn parser_recognises_create_publication_bare() {
29807 let s = parse("CREATE PUBLICATION pub_a");
29808 let Statement::CreatePublication(p) = s else {
29809 panic!("expected CreatePublication, got {s:?}")
29810 };
29811 assert_eq!(p.name, "pub_a");
29812 assert_eq!(p.scope, PublicationScope::AllTables);
29813 }
29814
29815 #[test]
29816 fn parser_recognises_create_publication_for_all_tables() {
29817 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29818 let Statement::CreatePublication(p) = s else {
29819 panic!("expected CreatePublication, got {s:?}")
29820 };
29821 assert_eq!(p.name, "pub_a");
29822 assert_eq!(p.scope, PublicationScope::AllTables);
29823 }
29824
29825 #[test]
29826 fn parser_recognises_drop_publication() {
29827 let s = parse("DROP PUBLICATION pub_a");
29828 let Statement::DropPublication { name, .. } = s else {
29829 panic!("expected DropPublication, got {s:?}")
29830 };
29831 assert_eq!(name, "pub_a");
29832 }
29833
29834 #[test]
29835 fn parser_recognises_for_table_list() {
29836 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29837 let Statement::CreatePublication(p) = s else {
29838 panic!("expected CreatePublication, got {s:?}")
29839 };
29840 assert_eq!(p.name, "pub_a");
29841 let PublicationScope::ForTables(ts) = p.scope else {
29842 panic!("expected ForTables scope")
29843 };
29844 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29845 }
29846
29847 #[test]
29848 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29849 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29850 // is rejected (`invalid publication object list`; the old
29851 // test pinned an unverifiable "PG 19 accepts both" claim);
29852 // TABLES pairs with IN SCHEMA.
29853 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29854 .expect_err("bare FOR TABLES must reject");
29855 assert!(
29856 alloc::format!("{err}").contains("invalid publication object list"),
29857 "got: {err}"
29858 );
29859 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29860 let Statement::CreatePublication(p) = s else {
29861 panic!("expected CreatePublication, got {s:?}")
29862 };
29863 let PublicationScope::TablesInSchema(schema) = p.scope else {
29864 panic!("expected TablesInSchema")
29865 };
29866 assert_eq!(schema, "public");
29867 }
29868
29869 #[test]
29870 fn parser_recognises_for_all_tables_except_list() {
29871 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29872 let Statement::CreatePublication(p) = s else {
29873 panic!()
29874 };
29875 let PublicationScope::AllTablesExcept(ts) = p.scope else {
29876 panic!("expected AllTablesExcept")
29877 };
29878 assert_eq!(ts, alloc::vec!["t1", "t2"]);
29879 }
29880
29881 #[test]
29882 fn parser_rejects_for_table_with_empty_list() {
29883 // `FOR TABLE` with nothing after is a parse error.
29884 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29885 .expect_err("must error on empty list");
29886 // No specific message asserted — the call falls through to
29887 // expect_ident_like which yields "expected identifier, got …".
29888 assert!(!err.message.is_empty());
29889 }
29890
29891 #[test]
29892 fn parser_recognises_show_publications() {
29893 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29894 // bare ident in this position, NOT a reserved keyword.
29895 let s = parse("SHOW PUBLICATIONS");
29896 assert!(matches!(s, Statement::ShowPublications));
29897 }
29898
29899 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29900
29901 #[test]
29902 fn parser_recognises_create_subscription_single_publication() {
29903 let s = parse(
29904 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29905 );
29906 let Statement::CreateSubscription(c) = s else {
29907 panic!("expected CreateSubscription, got {s:?}")
29908 };
29909 assert_eq!(c.name, "sub_a");
29910 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29911 assert_eq!(c.publications, alloc::vec!["pub_a"]);
29912 }
29913
29914 #[test]
29915 fn parser_recognises_create_subscription_multi_publication() {
29916 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29917 let Statement::CreateSubscription(c) = s else {
29918 panic!()
29919 };
29920 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29921 }
29922
29923 #[test]
29924 fn parser_rejects_create_subscription_missing_connection() {
29925 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29926 .expect_err("must error on missing CONNECTION");
29927 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29928 }
29929
29930 #[test]
29931 fn parser_rejects_create_subscription_missing_publication() {
29932 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29933 .expect_err("must error on missing PUBLICATION");
29934 assert_eq!(err.message, "syntax error at end of input");
29935 }
29936
29937 #[test]
29938 fn parser_recognises_drop_subscription() {
29939 let s = parse("DROP SUBSCRIPTION sub_a");
29940 let Statement::DropSubscription { name, .. } = s else {
29941 panic!("expected DropSubscription, got {s:?}")
29942 };
29943 assert_eq!(name, "sub_a");
29944 }
29945
29946 #[test]
29947 fn parser_recognises_show_subscriptions() {
29948 let s = parse("SHOW SUBSCRIPTIONS");
29949 assert!(matches!(s, Statement::ShowSubscriptions));
29950 }
29951
29952 #[test]
29953 fn parser_recognises_wait_for_wal_position_no_timeout() {
29954 let s = parse("WAIT FOR WAL POSITION 12345");
29955 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29956 panic!("expected WaitForWalPosition, got {s:?}")
29957 };
29958 assert_eq!(pos, 12345);
29959 assert!(timeout_ms.is_none());
29960 }
29961
29962 #[test]
29963 fn parser_recognises_wait_for_wal_position_with_timeout() {
29964 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29965 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29966 panic!()
29967 };
29968 assert_eq!(pos, 67890);
29969 assert_eq!(timeout_ms, Some(5000));
29970 }
29971
29972 #[test]
29973 fn parser_rejects_wait_with_negative_position() {
29974 // The lexer treats `-` as a token; `expect_u64_literal`
29975 // only sees the Integer that follows, so the negative
29976 // arrives as a unary-minus expression at higher levels.
29977 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29978 // parse error one way or another.
29979 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29980 assert!(!err.message.is_empty());
29981 }
29982
29983 #[test]
29984 fn parser_recognises_bare_analyze() {
29985 let s = parse("ANALYZE");
29986 assert!(matches!(s, Statement::Analyze(None)));
29987 }
29988
29989 #[test]
29990 fn parser_recognises_analyze_with_table() {
29991 let s = parse("ANALYZE users");
29992 let Statement::Analyze(Some(name)) = s else {
29993 panic!("expected Analyze, got {s:?}")
29994 };
29995 assert_eq!(name, "users");
29996 }
29997
29998 #[test]
29999 fn parser_recognises_analyze_with_quoted_table() {
30000 let s = parse("ANALYZE \"Mixed Case\"");
30001 let Statement::Analyze(Some(name)) = s else {
30002 panic!()
30003 };
30004 assert_eq!(name, "Mixed Case");
30005 }
30006
30007 #[test]
30008 fn parser_rejects_analyze_with_garbage_token() {
30009 let err = parse_statement("ANALYZE 42").expect_err("must error");
30010 assert!(!err.message.is_empty());
30011 }
30012
30013 #[test]
30014 fn analyze_display_roundtrips() {
30015 for sql in ["ANALYZE", "ANALYZE users"] {
30016 let s = parse(sql);
30017 let printed = s.to_string();
30018 let again = parse_statement(&printed)
30019 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30020 assert_eq!(s, again);
30021 }
30022 }
30023
30024 #[test]
30025 fn wait_for_display_roundtrips() {
30026 for sql in [
30027 "WAIT FOR WAL POSITION 12345",
30028 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
30029 ] {
30030 let s = parse(sql);
30031 let printed = s.to_string();
30032 let again = parse_statement(&printed)
30033 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30034 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30035 }
30036 }
30037
30038 #[test]
30039 fn subscription_ddl_display_roundtrips() {
30040 for sql in [
30041 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
30042 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
30043 "DROP SUBSCRIPTION sub_a",
30044 "SHOW SUBSCRIPTIONS",
30045 ] {
30046 let s = parse(sql);
30047 let printed = s.to_string();
30048 let again = parse_statement(&printed)
30049 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30050 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30051 }
30052 }
30053
30054 #[test]
30055 fn parser_drop_dispatches_user_vs_publication() {
30056 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
30057 // tokenises DROP. Both targets must still parse.
30058 let s = parse("DROP USER 'alice'");
30059 let Statement::DropUser { name, .. } = s else {
30060 panic!("expected DropUser, got {s:?}")
30061 };
30062 assert_eq!(name, "alice");
30063 // And DROP PUBLICATION lands the new variant.
30064 let s = parse("DROP PUBLICATION p1");
30065 assert!(matches!(s, Statement::DropPublication { .. }));
30066 }
30067
30068 #[test]
30069 fn publication_ddl_display_roundtrips() {
30070 // Every CREATE PUBLICATION variant must Display → parse →
30071 // same AST. v6.1.3 covers all three scope shapes.
30072 for sql in [
30073 "CREATE PUBLICATION pub_a",
30074 "CREATE PUBLICATION pub_a FOR ALL TABLES",
30075 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
30076 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
30077 "DROP PUBLICATION pub_a",
30078 "SHOW PUBLICATIONS",
30079 ] {
30080 let s = parse(sql);
30081 let printed = s.to_string();
30082 let again = parse_statement(&printed)
30083 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30084 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30085 }
30086 }
30087
30088 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
30089
30090 #[test]
30091 fn create_function_returns_trigger_plpgsql_minimal() {
30092 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
30093 let s = parse(sql);
30094 let Statement::CreateFunction(f) = s else {
30095 panic!("expected CreateFunction");
30096 };
30097 assert_eq!(f.name, "noop");
30098 assert!(!f.or_replace);
30099 assert!(f.args.is_empty());
30100 assert!(matches!(f.returns, FunctionReturn::Trigger));
30101 assert_eq!(f.language, "plpgsql");
30102 let FunctionBody::PlPgSql(block) = f.body else {
30103 panic!("expected PlPgSql body");
30104 };
30105 assert_eq!(block.statements.len(), 1);
30106 assert!(matches!(
30107 block.statements[0],
30108 PlPgSqlStmt::Return(ReturnTarget::New)
30109 ));
30110 }
30111
30112 #[test]
30113 fn create_function_or_replace_with_assignment() {
30114 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
30115 // RETURN NEW.
30116 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
30117BEGIN
30118 NEW.search_vector := to_tsvector('english', NEW.subject);
30119 RETURN NEW;
30120END;
30121$$";
30122 let s = parse(sql);
30123 let Statement::CreateFunction(f) = s else {
30124 panic!("expected CreateFunction");
30125 };
30126 assert!(f.or_replace);
30127 let FunctionBody::PlPgSql(block) = &f.body else {
30128 panic!("expected PlPgSql body");
30129 };
30130 assert_eq!(block.statements.len(), 2);
30131 // First statement: NEW.search_vector := to_tsvector(...)
30132 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
30133 panic!("expected Assign as first stmt");
30134 };
30135 match target {
30136 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
30137 other => panic!("expected NEW.col, got {other:?}"),
30138 }
30139 // Second statement: RETURN NEW
30140 assert!(matches!(
30141 block.statements[1],
30142 PlPgSqlStmt::Return(ReturnTarget::New)
30143 ));
30144 }
30145
30146 #[test]
30147 fn create_trigger_after_insert_or_update() {
30148 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
30149 let s = parse(sql);
30150 let Statement::CreateTrigger(t) = s else {
30151 panic!("expected CreateTrigger");
30152 };
30153 assert_eq!(t.name, "tg");
30154 assert_eq!(t.table, "messages");
30155 assert_eq!(t.timing, TriggerTiming::After);
30156 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
30157 assert_eq!(t.for_each, TriggerForEach::Row);
30158 assert_eq!(t.function, "update_sv");
30159 }
30160
30161 #[test]
30162 fn create_trigger_before_delete_execute_procedure_alias() {
30163 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
30164 let sql =
30165 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
30166 let s = parse(sql);
30167 let Statement::CreateTrigger(t) = s else {
30168 panic!("expected CreateTrigger");
30169 };
30170 assert_eq!(t.timing, TriggerTiming::Before);
30171 assert_eq!(t.events, vec![TriggerEvent::Delete]);
30172 }
30173
30174 #[test]
30175 fn drop_trigger_if_exists_round_trips() {
30176 // No parser support for DROP TRIGGER yet — added in v7.12.5
30177 // alongside the broader DROP …{IF EXISTS} cleanup. The
30178 // AST + Display impls are in place so we round-trip via
30179 // construction:
30180 let s = Statement::DropTrigger {
30181 name: "tg".into(),
30182 table: "messages".into(),
30183 if_exists: true,
30184 };
30185 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
30186 }
30187
30188 #[test]
30189 fn trigger_ddl_display_roundtrips_through_parser() {
30190 // CREATE TRIGGER + its referenced CREATE FUNCTION must
30191 // Display → parse → same AST (modulo PL/pgSQL body
30192 // formatting which is parser-canonicalised).
30193 for sql in [
30194 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
30195 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
30196 ] {
30197 let s = parse(sql);
30198 let printed = s.to_string();
30199 let again = parse_statement(&printed)
30200 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30201 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30202 }
30203 }
30204}