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 other => {
3036 // v7.38 (read01 P3.20) — a custom namespaced GUC
3037 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3038 // consume the dotted tail so it round-trips with
3039 // `SET app.foo` / `current_setting('app.foo')`.
3040 let mut full = other.to_string();
3041 while matches!(self.peek(), Token::Dot) {
3042 self.advance();
3043 let seg = self.expect_ident_like()?;
3044 full.push('.');
3045 full.push_str(&seg.to_ascii_lowercase());
3046 }
3047 Ok(Statement::ShowParameter(full))
3048 }
3049 }
3050 }
3051 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3052 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3053 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3054 // arrived as a bare ident; tokenising it dedicatedly
3055 // keeps the dispatch tree small.
3056 Token::Drop => {
3057 self.advance();
3058 match self.peek() {
3059 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3060 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3061 // around DROP ROLE cleanup. SPG has no role-owner
3062 // model, so consume to boundary as a no-op.
3063 Token::Ident(s) | Token::QuotedIdent(s)
3064 if s.eq_ignore_ascii_case("owned") =>
3065 {
3066 // v7.39 (round 696) — still a no-op (SPG has no
3067 // role-owner model), but the ROLE is carried out so
3068 // the engine can refuse one that does not exist,
3069 // which is what PG18 does.
3070 self.advance();
3071 if self.peek_is_by() {
3072 self.advance();
3073 }
3074 let names = self.take_comma_separated_names();
3075 self.consume_until_statement_boundary();
3076 Ok(Statement::ValidateOnly {
3077 kind: crate::ast::ValidateOnlyKind::RoleName,
3078 names,
3079 })
3080 }
3081 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3082 // It drops only a TEMPORARY table, and name resolution
3083 // already prefers the session's own, so the keyword is
3084 // consumed and the ordinary DROP TABLE path runs.
3085 Token::Ident(s) | Token::QuotedIdent(s)
3086 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3087 {
3088 self.advance();
3089 if !matches!(self.peek(), Token::Table) {
3090 return Err(self.err(alloc::format!(
3091 "expected TABLE after DROP TEMPORARY, got {:?}",
3092 self.peek()
3093 )));
3094 }
3095 self.parse_drop_table_after_keyword()
3096 }
3097 Token::Publication => {
3098 self.advance();
3099 // v7.39 (round 754, F31-B4) — the round-753
3100 // audit probe tripped over the missing
3101 // `IF EXISTS` here (syntax error).
3102 let if_exists = self.consume_if_exists();
3103 let name = self.expect_ident_or_string()?;
3104 Ok(Statement::DropPublication { name, if_exists })
3105 }
3106 Token::Subscription => {
3107 self.advance();
3108 let if_exists = self.consume_if_exists();
3109 let name = self.expect_ident_or_string()?;
3110 Ok(Statement::DropSubscription { name, if_exists })
3111 }
3112 Token::Ident(s) | Token::QuotedIdent(s)
3113 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3114 {
3115 self.advance();
3116 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3117 // login user IS a role in PG, and SPG's store holds
3118 // both. `IF EXISTS` is accepted on either spelling.
3119 let if_exists = self.consume_if_exists();
3120 let name = self.expect_ident_or_string()?;
3121 Ok(Statement::DropUser { name, if_exists })
3122 }
3123 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3124 // CREATE DATABASE has parsed since v7.14 and this did
3125 // not, so `DROP DATABASE IF EXISTS x` — what every
3126 // teardown script and pg_dumpall preamble opens with —
3127 // came back as a syntax error, which IF EXISTS cannot
3128 // soften. The name is carried so the engine can answer
3129 // the way PG does; PG never lets this succeed on a
3130 // single-database server, since the name is either
3131 // unknown ("database … does not exist", or a notice
3132 // under IF EXISTS) or the one you are connected to
3133 // ("cannot drop the currently open database").
3134 Token::Ident(s) | Token::QuotedIdent(s)
3135 if s.eq_ignore_ascii_case("database") =>
3136 {
3137 self.advance();
3138 let if_exists = self.consume_if_exists();
3139 let name = self.expect_ident_or_string()?;
3140 self.consume_until_statement_boundary();
3141 Ok(Statement::DropDatabase { name, if_exists })
3142 }
3143 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3144 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3145 self.advance();
3146 let if_exists = self.consume_if_exists();
3147 let name = self.expect_ident_like()?;
3148 // ON <table>
3149 if !matches!(self.peek(), Token::On) {
3150 return Err(self.err(alloc::format!(
3151 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3152 self.peek()
3153 )));
3154 }
3155 self.advance();
3156 let table = self.expect_ident_like()?;
3157 Ok(Statement::DropTrigger {
3158 name,
3159 table,
3160 if_exists,
3161 })
3162 }
3163 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3164 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3166 self.advance();
3167 let if_exists = self.consume_if_exists();
3168 let name = self.expect_ident_like()?;
3169 if !matches!(self.peek(), Token::On) {
3170 return Err(self.err(alloc::format!(
3171 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3172 self.peek()
3173 )));
3174 }
3175 self.advance();
3176 let table = self.expect_ident_like()?;
3177 // Optional CASCADE / RESTRICT — accepted, no effect.
3178 self.consume_until_statement_boundary();
3179 Ok(Statement::DropRule {
3180 name,
3181 table,
3182 if_exists,
3183 })
3184 }
3185 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3186 // v7.12.4 ignores any optional arg-list (signature-
3187 // based overload disambiguation lands in v7.12.5+).
3188 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3189 self.advance();
3190 let if_exists = self.consume_if_exists();
3191 let name = self.expect_ident_like()?;
3192 // v7.39 (read01 round 62) — the argument list identifies
3193 // WHICH overload to drop, so it is captured, not
3194 // discarded. `DROP FUNCTION f` (no list) is legal when
3195 // the name is unambiguous; the engine enforces that.
3196 let args = if matches!(self.peek(), Token::LParen) {
3197 Some(self.parse_function_signature_types()?)
3198 } else {
3199 None
3200 };
3201 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3202 // trailer, which `DROP TABLE` and `DROP INDEX` have
3203 // accepted since v7.14 and this one refused outright.
3204 // pg_dump writes it, so refusing was a parse error in
3205 // the middle of a restore. SPG drops the function
3206 // either way — it tracks no dependents to cascade to —
3207 // which is the same reading the other two give it.
3208 self.consume_drop_behaviour();
3209 Ok(Statement::DropFunction {
3210 name,
3211 args,
3212 if_exists,
3213 })
3214 }
3215 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3216 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3217 // emit DROP TABLE IF EXISTS at the head of every
3218 // CREATE TABLE block so re-importing a dump
3219 // overwrites prior state. SPG accepts and removes
3220 // matching tables; CASCADE/RESTRICT trailers
3221 // accepted silently.
3222 Token::Table => self.parse_drop_table_after_keyword(),
3223 // v7.14.0 — DROP INDEX [IF EXISTS] name
3224 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3225 // for partial-index renames and pgvector
3226 // migrations. SPG removes the matching index;
3227 // IF EXISTS makes the drop idempotent.
3228 Token::Index => {
3229 self.advance();
3230 let if_exists_at = self.pos;
3231 let if_exists = self.consume_if_exists();
3232 let name = self.expect_ident_like()?;
3233 // v7.39.7 — MySQL's own spelling, which SPG
3234 // refused.
3235 //
3236 // `DROP INDEX i ON t` is how MySQL drops an
3237 // index; its names live inside a table, so the
3238 // statement names the table. Measured against
3239 // MySQL 9.7.2: the form above works, and the
3240 // bare `DROP INDEX i` PostgreSQL uses is a 1064
3241 // there. SPG had it exactly backwards on the
3242 // MySQL wire — the bare form accepted, MySQL's
3243 // own a syntax error — so a migration that drops
3244 // an index failed against the drop-in and not
3245 // against the thing it replaces.
3246 let table = if matches!(self.peek(), Token::On) {
3247 self.advance();
3248 Some(self.expect_ident_like()?)
3249 } else {
3250 None
3251 };
3252 if self.mysql_dialect {
3253 // MySQL has no `IF EXISTS` here either:
3254 // `DROP INDEX IF EXISTS i ON t` is a 1064.
3255 if if_exists {
3256 return Err(self.err_at(
3257 if_exists_at,
3258 "MySQL has no IF EXISTS on DROP INDEX".into(),
3259 ));
3260 }
3261 if table.is_none() {
3262 return Err(self.err("expected ON after the index name".into()));
3263 }
3264 }
3265 if matches!(
3266 self.peek(),
3267 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3268 || s.eq_ignore_ascii_case("restrict")
3269 ) {
3270 self.advance();
3271 }
3272 Ok(Statement::DropIndex {
3273 name,
3274 if_exists,
3275 table,
3276 })
3277 }
3278 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3279 // [CASCADE|RESTRICT]. SPG is single-database;
3280 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3281 // name [, name…] [CASCADE | RESTRICT]. Real
3282 // unregister (was silent no-op pre-v7.17).
3283 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3284 self.advance();
3285 let if_exists = self.consume_if_exists();
3286 let mut names = vec![self.expect_ident_like()?];
3287 while matches!(self.peek(), Token::Comma) {
3288 self.advance();
3289 names.push(self.expect_ident_like()?);
3290 }
3291 if matches!(
3292 self.peek(),
3293 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3294 || s.eq_ignore_ascii_case("restrict")
3295 ) {
3296 self.advance();
3297 }
3298 Ok(Statement::DropSchema { names, if_exists })
3299 }
3300 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3301 // name [, name…] [CASCADE|RESTRICT].
3302 Token::Ident(s) | Token::QuotedIdent(s)
3303 if s.eq_ignore_ascii_case("type") =>
3304 {
3305 self.advance();
3306 let if_exists = self.consume_if_exists();
3307 let mut names = vec![self.expect_ident_like()?];
3308 while matches!(self.peek(), Token::Comma) {
3309 self.advance();
3310 names.push(self.expect_ident_like()?);
3311 }
3312 if matches!(
3313 self.peek(),
3314 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3315 || s.eq_ignore_ascii_case("restrict")
3316 ) {
3317 self.advance();
3318 }
3319 Ok(Statement::DropType { names, if_exists })
3320 }
3321 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3322 // name [, name…] [CASCADE|RESTRICT].
3323 Token::Ident(s) | Token::QuotedIdent(s)
3324 if s.eq_ignore_ascii_case("domain") =>
3325 {
3326 self.advance();
3327 let if_exists = self.consume_if_exists();
3328 let mut names = vec![self.expect_ident_like()?];
3329 while matches!(self.peek(), Token::Comma) {
3330 self.advance();
3331 names.push(self.expect_ident_like()?);
3332 }
3333 if matches!(
3334 self.peek(),
3335 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3336 || s.eq_ignore_ascii_case("restrict")
3337 ) {
3338 self.advance();
3339 }
3340 Ok(Statement::DropDomain { names, if_exists })
3341 }
3342 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3343 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3344 Token::Ident(s) | Token::QuotedIdent(s)
3345 if s.eq_ignore_ascii_case("materialized") =>
3346 {
3347 self.advance();
3348 let nxt = self.peek().clone();
3349 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3350 {
3351 return Err(self.err(alloc::format!(
3352 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3353 )));
3354 }
3355 self.advance();
3356 let if_exists = self.consume_if_exists();
3357 let mut names = vec![self.expect_ident_like()?];
3358 while matches!(self.peek(), Token::Comma) {
3359 self.advance();
3360 names.push(self.expect_ident_like()?);
3361 }
3362 if matches!(
3363 self.peek(),
3364 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3365 || s.eq_ignore_ascii_case("restrict")
3366 ) {
3367 self.advance();
3368 }
3369 Ok(Statement::DropMaterializedView { names, if_exists })
3370 }
3371 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3372 // name [, name…] [CASCADE|RESTRICT].
3373 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3374 self.advance();
3375 let if_exists = self.consume_if_exists();
3376 let mut names = vec![self.expect_ident_like()?];
3377 while matches!(self.peek(), Token::Comma) {
3378 self.advance();
3379 names.push(self.expect_ident_like()?);
3380 }
3381 if matches!(
3382 self.peek(),
3383 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3384 || s.eq_ignore_ascii_case("restrict")
3385 ) {
3386 self.advance();
3387 }
3388 Ok(Statement::DropView { names, if_exists })
3389 }
3390 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3391 // [CASCADE|RESTRICT]. Real removal from catalog
3392 // (was a silent no-op pre-v7.17).
3393 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3394 self.advance();
3395 let if_exists = self.consume_if_exists();
3396 let mut names = vec![self.expect_ident_like()?];
3397 while matches!(self.peek(), Token::Comma) {
3398 self.advance();
3399 names.push(self.expect_ident_like()?);
3400 }
3401 if matches!(
3402 self.peek(),
3403 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3404 || s.eq_ignore_ascii_case("restrict")
3405 ) {
3406 self.advance();
3407 }
3408 Ok(Statement::DropSequence { names, if_exists })
3409 }
3410 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3411 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3412 self.advance();
3413 self.parse_drop_policy_after_keyword()
3414 }
3415 // v7.37.17 (17.6 siblings) — DROP <target> for
3416 // targets SPG doesn't natively track. pg_dump
3417 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3418 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3419 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3420 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3421 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3422 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3423 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3424 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3425 // etc. — accept + Empty-return so pg_dump tails
3426 // load through. Materialized-view drop dispatches
3427 // to the existing DropTable path when the token
3428 // is Materialized-View-shaped (elsewhere in
3429 // this parser).
3430 Token::Ident(s) | Token::QuotedIdent(s)
3431 if s.eq_ignore_ascii_case("text")
3432 // The DROP dispatch matches on PEEK — `text` is
3433 // not yet consumed, so SEARCH/CONFIGURATION sit
3434 // at pos+1/pos+2 (the round-695 trap's mirror).
3435 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3436 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3437 {
3438 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3439 // validates the name; DICTIONARY / PARSER / TEMPLATE
3440 // stay in the noise arm below.
3441 self.advance(); // TEXT
3442 self.advance(); // SEARCH
3443 self.advance(); // CONFIGURATION
3444 let if_exists = self.consume_if_exists();
3445 let names = self.take_comma_separated_names();
3446 self.consume_until_statement_boundary();
3447 if if_exists {
3448 return Ok(Statement::Empty);
3449 }
3450 Ok(Statement::ValidateOnly {
3451 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3452 names,
3453 })
3454 }
3455 Token::Ident(s) | Token::QuotedIdent(s)
3456 if matches!(
3457 s.to_ascii_lowercase().as_str(),
3458 "type"
3459 | "domain"
3460 | "operator"
3461 | "cast"
3462 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3463 // TEMPLATE (CONFIGURATION intercepted above).
3464 | "text"
3465 | "materialized"
3466 | "large"
3467 | "role"
3468 | "access"
3469 | "procedure"
3470 | "routine"
3471 ) =>
3472 {
3473 self.consume_until_statement_boundary();
3474 Ok(Statement::Empty)
3475 }
3476 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3477 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3478 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3479 // foreign-data warning family (round 706) so a
3480 // CREATE→DROP sequence in a dump stays consistent.
3481 Token::Ident(s) | Token::QuotedIdent(s)
3482 if s.eq_ignore_ascii_case("server")
3483 || s.eq_ignore_ascii_case("foreign") =>
3484 {
3485 self.advance();
3486 self.consume_until_statement_boundary();
3487 Ok(Statement::ValidateOnly {
3488 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3489 names: Vec::new(),
3490 })
3491 }
3492 Token::Ident(s) | Token::QuotedIdent(s)
3493 if s.eq_ignore_ascii_case("collation")
3494 || s.eq_ignore_ascii_case("tablespace") =>
3495 {
3496 let kind = if s.eq_ignore_ascii_case("collation") {
3497 crate::ast::ValidateOnlyKind::CollationName
3498 } else {
3499 crate::ast::ValidateOnlyKind::TablespaceName
3500 };
3501 self.advance();
3502 let if_exists = self.consume_if_exists();
3503 let names = self.take_comma_separated_names();
3504 self.consume_until_statement_boundary();
3505 if if_exists {
3506 return Ok(Statement::Empty);
3507 }
3508 Ok(Statement::ValidateOnly { kind, names })
3509 }
3510 Token::Ident(s) | Token::QuotedIdent(s)
3511 if s.eq_ignore_ascii_case("event") =>
3512 {
3513 self.advance();
3514 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3515 {
3516 self.advance();
3517 }
3518 let if_exists = self.consume_if_exists();
3519 let names = self.take_comma_separated_names();
3520 self.consume_until_statement_boundary();
3521 if if_exists {
3522 return Ok(Statement::Empty);
3523 }
3524 Ok(Statement::ValidateOnly {
3525 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3526 names,
3527 })
3528 }
3529 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3530 // leave the noise list; see the ValidateOnly kinds.
3531 Token::Ident(s) | Token::QuotedIdent(s)
3532 if s.eq_ignore_ascii_case("conversion")
3533 || s.eq_ignore_ascii_case("language")
3534 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3535 // FIRST — the first draft looked for it after.
3536 || s.eq_ignore_ascii_case("procedural") =>
3537 {
3538 let kind = if s.eq_ignore_ascii_case("conversion") {
3539 crate::ast::ValidateOnlyKind::ConversionName
3540 } else {
3541 crate::ast::ValidateOnlyKind::LanguageName
3542 };
3543 self.advance();
3544 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
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 { kind, names })
3555 }
3556 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3557 // name(argtypes)[, …]`. Parsed for real so the engine
3558 // can answer as PG does; see Statement::DropAggregate.
3559 Token::Ident(s) | Token::QuotedIdent(s)
3560 if s.eq_ignore_ascii_case("aggregate") =>
3561 {
3562 self.advance();
3563 let if_exists = self.consume_if_exists();
3564 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3565 loop {
3566 let name = self.expect_ident_like()?;
3567 if !matches!(self.peek(), Token::LParen) {
3568 return Err(self.err(alloc::format!(
3569 "expected argument list after DROP AGGREGATE {name}"
3570 )));
3571 }
3572 self.advance();
3573 let mut args: Vec<String> = Vec::new();
3574 let mut star = false;
3575 loop {
3576 match self.peek().clone() {
3577 Token::RParen => {
3578 self.advance();
3579 break;
3580 }
3581 Token::Star => {
3582 self.advance();
3583 star = true;
3584 }
3585 Token::Comma => {
3586 self.advance();
3587 }
3588 _ => {
3589 // A type name may be multi-token
3590 // (`double precision`); glue idents
3591 // until , or ).
3592 let mut t = self.expect_ident_like()?;
3593 while let Token::Ident(nx) = self.peek() {
3594 let nx = nx.clone();
3595 self.advance();
3596 t.push(' ');
3597 t.push_str(&nx);
3598 }
3599 args.push(t);
3600 }
3601 }
3602 }
3603 items.push((name, if star { None } else { Some(args) }));
3604 if matches!(self.peek(), Token::Comma) {
3605 self.advance();
3606 } else {
3607 break;
3608 }
3609 }
3610 self.consume_until_statement_boundary();
3611 Ok(Statement::DropAggregate { if_exists, items })
3612 }
3613 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3614 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3615 // installed; `IF EXISTS` is the spelling that says do
3616 // not, and it keeps the no-op.
3617 Token::Ident(s) | Token::QuotedIdent(s)
3618 if s.eq_ignore_ascii_case("extension") =>
3619 {
3620 self.advance();
3621 let if_exists = self.consume_if_exists();
3622 let names = self.take_comma_separated_names();
3623 self.consume_until_statement_boundary();
3624 if if_exists {
3625 return Ok(Statement::Empty);
3626 }
3627 Ok(Statement::ValidateOnly {
3628 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3629 names,
3630 })
3631 }
3632 Token::Ident(s) | Token::QuotedIdent(s)
3633 if s.eq_ignore_ascii_case("statistics") =>
3634 {
3635 self.parse_drop_statistics_after_drop()
3636 }
3637 other => Err(self.err(format!(
3638 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3639 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3640 ))),
3641 }
3642 }
3643 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3644 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3645 // and accepted before the view name. SPG materialised
3646 // views re-evaluate on read (always-fresh semantics), so
3647 // the CONCURRENTLY-vs-serial distinction has no runtime
3648 // effect — the refresh body does not block readers either
3649 // way. Same accept-and-no-op pattern as DETACH PARTITION
3650 // CONCURRENTLY (16.5).
3651 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3652 self.advance();
3653 let nxt = self.peek().clone();
3654 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3655 {
3656 return Err(self.err(alloc::format!(
3657 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3658 )));
3659 }
3660 self.advance();
3661 let nxt2 = self.peek().clone();
3662 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3663 {
3664 return Err(self.err(alloc::format!(
3665 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3666 )));
3667 }
3668 self.advance();
3669 // Optional CONCURRENTLY noise word — consumed without
3670 // changing semantics.
3671 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3672 {
3673 self.advance();
3674 }
3675 let name = self.expect_ident_like()?;
3676 let with_data = self.parse_optional_with_data(true)?;
3677 Ok(Statement::RefreshMaterializedView { name, with_data })
3678 }
3679 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3680 self.advance();
3681 self.parse_update_after_keyword()
3682 }
3683 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3684 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3685 // [CASCADE | RESTRICT]. Clears every row from each named
3686 // table. Parses at the top level; the engine dispatcher
3687 // walks Statement::Truncate.
3688 // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3689 //
3690 // PostgreSQL renames a table through `ALTER TABLE … RENAME
3691 // TO`, which SPG already had, so this spelling answered 1064
3692 // — and it is what a MySQL migration writes. Measured on
3693 // 9.7.2: several pairs in one statement are accepted, and
3694 // renaming onto a name that exists is 1050.
3695 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3696 self.advance();
3697 if matches!(self.peek(), Token::Table) {
3698 self.advance();
3699 }
3700 let mut pairs: Vec<(String, String)> = Vec::new();
3701 loop {
3702 let from = self.expect_ident_like()?;
3703 if matches!(self.peek(), Token::To) {
3704 self.advance();
3705 } else {
3706 self.expect_keyword_ident("to")?;
3707 }
3708 let to = self.expect_ident_like()?;
3709 pairs.push((from, to));
3710 if matches!(self.peek(), Token::Comma) {
3711 self.advance();
3712 } else {
3713 break;
3714 }
3715 }
3716 Ok(Statement::RenameTables(pairs))
3717 }
3718 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3719 self.advance();
3720 // Optional TABLE noise word — PG accepts both the reserved
3721 // token and the bare identifier spelling.
3722 if matches!(self.peek(), Token::Table)
3723 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3724 {
3725 self.advance();
3726 }
3727 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3728 // not absorbed. The lookahead keeps a table genuinely
3729 // called `only` working: the keyword is a keyword only
3730 // when a name follows it.
3731 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3732 if s.eq_ignore_ascii_case("only"))
3733 && matches!(
3734 self.tokens.get(self.pos + 1),
3735 Some(Token::Ident(_) | Token::QuotedIdent(_))
3736 );
3737 if only {
3738 self.advance();
3739 }
3740 // Table names (comma-separated).
3741 let mut tables = Vec::new();
3742 loop {
3743 tables.push(self.expect_ident_like()?);
3744 if matches!(self.peek(), Token::Comma) {
3745 self.advance();
3746 continue;
3747 }
3748 break;
3749 }
3750 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3751 let mut restart_identity = false;
3752 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3753 {
3754 self.advance();
3755 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3756 {
3757 self.advance();
3758 restart_identity = true;
3759 }
3760 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3761 {
3762 self.advance();
3763 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3764 {
3765 self.advance();
3766 }
3767 }
3768 // Optional CASCADE / RESTRICT.
3769 let mut cascade = false;
3770 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3771 {
3772 self.advance();
3773 cascade = true;
3774 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3775 {
3776 self.advance();
3777 }
3778 Ok(Statement::Truncate {
3779 tables,
3780 restart_identity,
3781 cascade,
3782 only,
3783 })
3784 }
3785 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3786 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3787 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3788 // rows change so the index tree is always up-to-date;
3789 // REINDEX is a strict no-op. Accept the whole statement
3790 // shape to boundary for pg_dump round-trip compatibility.
3791 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3792 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3793 // index bloat to rebuild, so the work stays a no-op, but PG
3794 // validates what it was pointed at and this swallowed the
3795 // name at parse time — `REINDEX TABLE typo` reported
3796 // success. Measured on PG18: INDEX / TABLE name a relation,
3797 // SCHEMA a schema, SYSTEM nothing.
3798 self.advance();
3799 self.parse_reindex_tail()
3800 }
3801 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3802 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3803 // SPG has no MVCC bloat today (Phase D visibility map
3804 // queues with v7.38); the freezer collapses hot-tier
3805 // rows into cold segments automatically. VACUUM is a
3806 // no-op — pg_dump maintenance scripts and Discourse's
3807 // periodic-maintenance path both emit it.
3808 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3809 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3810 // actual bloat, so the pre-MVCC accept-and-ignore posture
3811 // became a silent no-op on a customer's manual reclaim.
3812 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3813 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3814 // ANALYZE is captured, the optional table name is captured.
3815 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3816 self.advance();
3817 // Parenthesised option list: absorb it.
3818 if matches!(self.peek(), Token::LParen) {
3819 let mut depth = 0usize;
3820 loop {
3821 match self.advance() {
3822 Token::LParen => depth += 1,
3823 Token::RParen => {
3824 depth -= 1;
3825 if depth == 0 {
3826 break;
3827 }
3828 }
3829 Token::Eof => break,
3830 _ => {}
3831 }
3832 }
3833 }
3834 let mut analyze = false;
3835 let mut table: Option<String> = None;
3836 loop {
3837 match self.peek() {
3838 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3839 // an identifier, so the loop below broke out on it and
3840 // dropped the table name: `VACUUM FULL nosuch` was
3841 // accepted where `VACUUM nosuch` was refused.
3842 Token::Full => {
3843 self.advance();
3844 }
3845 Token::Ident(w) | Token::QuotedIdent(w) => {
3846 let wl = w.to_ascii_lowercase();
3847 match wl.as_str() {
3848 "full" | "freeze" | "verbose" => {
3849 self.advance();
3850 }
3851 "analyze" | "analyse" => {
3852 analyze = true;
3853 self.advance();
3854 }
3855 _ => {
3856 table = Some(self.expect_ident_like()?);
3857 break;
3858 }
3859 }
3860 }
3861 _ => break,
3862 }
3863 }
3864 // Optional trailing column list / anything else to the
3865 // statement boundary (PG accepts per-column ANALYZE).
3866 self.consume_until_statement_boundary();
3867 Ok(Statement::Vacuum { table, analyze })
3868 }
3869 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3870 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3871 // <index>. PG stores rows in physical order matching
3872 // an index; SPG's hot-tier is append-only + cold-tier
3873 // is segment-frozen, so clustering has no persistent
3874 // effect. Accept-and-no-op for pg_dump compat.
3875 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3876 // v7.39 (round 535) — same as REINDEX above: the relation is
3877 // carried so the engine can refuse one that does not exist.
3878 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3879 self.advance();
3880 self.parse_cluster_tail()
3881 }
3882 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3883 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3884 // optional string payload; UNLISTEN takes a channel or `*`.
3885 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3886 self.advance();
3887 let ch = match self.advance() {
3888 Token::Ident(c) | Token::QuotedIdent(c) => c,
3889 other => {
3890 return Err(self.err(format!(
3891 "expected channel name after LISTEN, got {other:?}"
3892 )));
3893 }
3894 };
3895 Ok(Statement::Listen(ch))
3896 }
3897 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3898 self.advance();
3899 let channel = match self.advance() {
3900 Token::Ident(c) | Token::QuotedIdent(c) => c,
3901 other => {
3902 return Err(self.err(format!(
3903 "expected channel name after NOTIFY, got {other:?}"
3904 )));
3905 }
3906 };
3907 let payload = if matches!(self.peek(), Token::Comma) {
3908 self.advance();
3909 match self.advance() {
3910 Token::String(p) => Some(p),
3911 other => {
3912 return Err(self.err(format!(
3913 "expected string payload after NOTIFY <channel>, got {other:?}"
3914 )));
3915 }
3916 }
3917 } else {
3918 None
3919 };
3920 Ok(Statement::Notify { channel, payload })
3921 }
3922 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3923 self.advance();
3924 match self.advance() {
3925 Token::Star => Ok(Statement::Unlisten(None)),
3926 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3927 other => Err(self.err(format!(
3928 "expected channel name or * after UNLISTEN, got {other:?}"
3929 ))),
3930 }
3931 }
3932 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3933 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3934 // process-wide write lock today; explicit LOCK has no
3935 // effect. Accept-and-no-op for pg_dump / migration
3936 // compat.
3937 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3938 self.advance();
3939 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3940 // engine holds a process-wide write lock), but the TABLE
3941 // NAME is now carried out so the engine can refuse one that
3942 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3943 // READ|WRITE` is a different statement with the same first
3944 // word; it keeps the old no-op, because a MySQL dump's
3945 // bracket names tables it is about to create.
3946 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3947 if k.eq_ignore_ascii_case("tables"));
3948 if mysql_tables {
3949 self.consume_until_statement_boundary();
3950 return Ok(Statement::Empty);
3951 }
3952 if matches!(self.peek(), Token::Table) {
3953 self.advance();
3954 }
3955 let names = self.take_comma_separated_names();
3956 self.consume_until_statement_boundary();
3957 Ok(Statement::ValidateOnly {
3958 kind: crate::ast::ValidateOnlyKind::LockTable,
3959 names,
3960 })
3961 }
3962 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3963 // durability marker + snapshot in PG. SPG has WAL
3964 // checkpointing on a byte / time schedule (v7.37.10
3965 // 60s / 4 MiB defaults). The bare statement parses to
3966 // `Statement::Empty` here (the no_std engine owns no
3967 // WAL / snapshot); v7.37 Epic Du wires the HOST
3968 // (embedded `Database::execute_buffered`, via
3969 // `sql_is_checkpoint`) to force an immediate synchronous
3970 // checkpoint through `Database::checkpoint` — a real
3971 // durability barrier, matching PG.
3972 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3973 self.advance();
3974 self.consume_until_statement_boundary();
3975 Ok(Statement::Empty)
3976 }
3977 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3978 self.advance();
3979 self.parse_delete_after_keyword()
3980 }
3981 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3982 // ALTER is not a reserved keyword in the lexer — handled
3983 // as a bare ident here.
3984 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3985 self.advance();
3986 self.parse_alter_after_keyword()
3987 }
3988 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3989 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3990 // additions needed.
3991 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3992 self.advance();
3993 self.parse_wait_after_keyword()
3994 }
3995 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3996 // Bare ANALYZE → analyse every user table; ANALYZE
3997 // <name> → re-stats one. The argument is an optional
3998 // ident (or quoted ident); anything else is a parse
3999 // error.
4000 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
4001 // `WHERE` filter (carved out per V6_7_DESIGN.md
4002 // STABILITY). Lex order: identifier "compact" → "cold"
4003 // → "segments". Anything else after `COMPACT` is a
4004 // parse error.
4005 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
4006 self.advance();
4007 let next = self.peek().clone();
4008 let cold = match next {
4009 Token::Ident(s) | Token::QuotedIdent(s) => s,
4010 _ => {
4011 return Err(
4012 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
4013 );
4014 }
4015 };
4016 if !cold.eq_ignore_ascii_case("cold") {
4017 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
4018 }
4019 self.advance();
4020 let next = self.peek().clone();
4021 let segments = match next {
4022 Token::Ident(s) | Token::QuotedIdent(s) => s,
4023 _ => {
4024 return Err(self.err(format!(
4025 "expected SEGMENTS after COMPACT COLD, got {:?}",
4026 self.peek()
4027 )));
4028 }
4029 };
4030 if !segments.eq_ignore_ascii_case("segments") {
4031 return Err(self.err(format!(
4032 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4033 )));
4034 }
4035 self.advance();
4036 Ok(Statement::CompactColdSegments)
4037 }
4038 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4039 // Parsed as a case-insensitive identifier since MERGE
4040 // isn't a reserved lexer keyword (collides with the
4041 // mysqldump `ALGORITHM = MERGE` view clause if it
4042 // were); the inner parser drives the rest of the
4043 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4044 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4045 self.advance();
4046 self.parse_merge_after_keyword()
4047 }
4048 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4049 self.advance();
4050 // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4051 // keyword is noise to the parse; what differs is the
4052 // ANSWER, which MySQL returns as a result set — see the
4053 // executor.
4054 let mysql_table_kw = matches!(self.peek(), Token::Table);
4055 if mysql_table_kw {
4056 self.advance();
4057 }
4058 let target = match self.peek() {
4059 Token::Eof | Token::Semicolon => None,
4060 Token::Ident(_) | Token::QuotedIdent(_) => {
4061 Some(self.expect_ident_like()?)
4062 }
4063 other => {
4064 return Err(self.err(format!(
4065 "expected table name or end of statement after ANALYZE, got {other:?}"
4066 )));
4067 }
4068 };
4069 // v7.39 (round 776, F31 J7) — the per-column form
4070 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4071 // here while the VACUUM arm already consumed it; SPG
4072 // analyzes whole tables, so the list parses and is
4073 // accepted like the VACUUM path's.
4074 if target.is_some() && matches!(self.peek(), Token::LParen) {
4075 self.advance();
4076 loop {
4077 let _ = self.expect_ident_like()?;
4078 match self.peek() {
4079 Token::Comma => {
4080 self.advance();
4081 }
4082 Token::RParen => {
4083 self.advance();
4084 break;
4085 }
4086 other => {
4087 return Err(self.err(format!(
4088 "expected ',' or ')' in ANALYZE column list, got {other:?}"
4089 )));
4090 }
4091 }
4092 }
4093 }
4094 Ok(Statement::Analyze(target))
4095 }
4096 // v7.12.1 — `SET <name> [TO|=] <value>`. The
4097 // `default_text_search_config` parameter is consumed
4098 // by the FTS function dispatcher; other parameter
4099 // names are recorded but treated as a no-op so PG
4100 // dump output loads.
4101 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4102 self.advance();
4103 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4104 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4105 // …` which the SessionVar path handles). `LOCAL` is the only
4106 // one that changes semantics — it scopes the change to the
4107 // current transaction — so capture it; SESSION / GLOBAL are
4108 // accepted and treated as the default session scope.
4109 let mut set_local = false;
4110 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4111 let q = s.to_ascii_lowercase();
4112 if q == "local" || q == "session" || q == "global" {
4113 set_local = q == "local";
4114 self.advance();
4115 }
4116 }
4117 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4118 // { DEFAULT | <role> }`. pg_dump's ACL section switches
4119 // to the object owner with it. SPG maps it onto the
4120 // session-role machinery (recorded delta RD-10: PG moves
4121 // session_user too; SPG moves the effective role).
4122 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4123 if s.eq_ignore_ascii_case("authorization"))
4124 {
4125 self.advance(); // AUTHORIZATION
4126 let role = match self.peek().clone() {
4127 Token::Default => {
4128 self.advance();
4129 None
4130 }
4131 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4132 self.advance();
4133 Some(s)
4134 }
4135 _ => None,
4136 };
4137 return Ok(Statement::SetRole(role));
4138 }
4139 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4140 // <collation>]` — change the connection client
4141 // charset. SPG stores UTF-8 always and orders
4142 // bytewise; accept as a no-op.
4143 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4144 {
4145 self.advance();
4146 // v7.39 — this used to parse the clause and throw it
4147 // away ("SPG stores UTF-8 always and orders
4148 // bytewise; accept as a no-op"). That sentence
4149 // stopped being true when collations arrived, and
4150 // once `collation_connection` began driving literal
4151 // comparison, dropping the COLLATE clause became a
4152 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4153 // utf8mb4_general_ci` reported back
4154 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4155 //
4156 // The charset name is emitted as `names` and the
4157 // ENGINE expands it, because which collation a
4158 // charset defaults to is MySQL semantics and belongs
4159 // beside the rest of them, not in the parser.
4160 let mut pairs = alloc::vec::Vec::new();
4161 if matches!(
4162 self.peek(),
4163 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4164 ) {
4165 let charset = match self.advance() {
4166 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4167 _ => unreachable!("peeked an ident-or-string"),
4168 };
4169 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4170 }
4171 // Optional `COLLATE <name>` — emitted AFTER `names`
4172 // so it overrides the charset's default, which is
4173 // what MySQL does.
4174 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4175 {
4176 self.advance();
4177 if matches!(
4178 self.peek(),
4179 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4180 ) {
4181 let coll = match self.advance() {
4182 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4183 _ => unreachable!("peeked an ident-or-string"),
4184 };
4185 pairs.push((
4186 String::from("collation_connection"),
4187 crate::ast::SetValue::Ident(coll),
4188 ));
4189 }
4190 }
4191 if pairs.is_empty() {
4192 return Ok(Statement::Empty);
4193 }
4194 return Ok(Statement::SetParameterList(pairs));
4195 }
4196 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4197 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4198 // uses this to switch to the object owner before
4199 // recreating tables. SPG has no role system so this
4200 // is a no-op.
4201 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4202 {
4203 self.advance(); // ROLE
4204 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4205 // reset to the login identity; a name / string sets the
4206 // effective role that drives current_user + RLS.
4207 let role = match self.peek().clone() {
4208 Token::Default => {
4209 self.advance();
4210 None
4211 }
4212 Token::Ident(s) | Token::QuotedIdent(s)
4213 if s.eq_ignore_ascii_case("none") =>
4214 {
4215 self.advance();
4216 None
4217 }
4218 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4219 self.advance();
4220 Some(s)
4221 }
4222 _ => None,
4223 };
4224 return Ok(Statement::SetRole(role));
4225 }
4226 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4227 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4228 // ISO SQL surface). pg_dump prepends this to fix
4229 // the isolation level for the restore session. SPG
4230 // defaults to READ COMMITTED and doesn't yet honor
4231 // session-set isolation across statements — accept
4232 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4233 // per-tx form is handled elsewhere.
4234 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4235 {
4236 self.advance(); // CHARACTERISTICS
4237 if matches!(self.peek(), Token::As) {
4238 self.advance();
4239 }
4240 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4241 self.advance();
4242 }
4243 // v7.39 — no longer a no-op. The note above said SPG
4244 // "doesn't yet honor session-set isolation across
4245 // statements"; it does now, through
4246 // `default_transaction_isolation`, and measured on
4247 // PG 18.6 this statement is exactly a way to set it:
4248 //
4249 // SET SESSION CHARACTERISTICS AS TRANSACTION
4250 // ISOLATION LEVEL REPEATABLE READ;
4251 // current_setting('default_transaction_isolation')
4252 // -> repeatable read
4253 //
4254 // pg_dump prepends this to fix the level for a
4255 // restore session, so accepting it and doing nothing
4256 // meant the restore ran at a level nobody chose.
4257 //
4258 // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4259 // still consumed and dropped. `default_transaction_read_only`
4260 // exists in the GUC inventory but nothing enforces it,
4261 // and setting a value no code honours is the very
4262 // defect this version is about — a session told it
4263 // holds a guarantee it does not.
4264 let modes = self.parse_isolation_level_clauses()?;
4265 self.consume_until_statement_boundary();
4266 let mut pairs: alloc::vec::Vec<(
4267 alloc::string::String,
4268 crate::ast::SetValue,
4269 )> = alloc::vec::Vec::new();
4270 if let Some(level) = modes.isolation {
4271 pairs.push((
4272 alloc::string::String::from("default_transaction_isolation"),
4273 crate::ast::SetValue::String(alloc::string::String::from(
4274 level.as_pg_str(),
4275 )),
4276 ));
4277 }
4278 if let Some(ro) = modes.read_only {
4279 pairs.push((
4280 alloc::string::String::from("default_transaction_read_only"),
4281 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4282 "on"
4283 } else {
4284 "off"
4285 })),
4286 ));
4287 }
4288 return Ok(if pairs.is_empty() {
4289 Statement::Empty
4290 } else {
4291 Statement::SetParameterList(pairs)
4292 });
4293 }
4294 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4295 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4296 // pg_dump emits this to control the deferrability of
4297 // FK / UNIQUE constraints across a bulk restore. SPG
4298 // has no deferrable-constraint machinery today; the
4299 // FK checker is strict-immediate. Accept-and-no-op
4300 // for pg_dump round-trip compatibility.
4301 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4302 {
4303 self.advance(); // CONSTRAINTS
4304 // v7.39 (round 288) — no longer a no-op: the trailing
4305 // DEFERRED / IMMEDIATE sets the transaction's timing.
4306 // v7.39 (round 308, V29) — and the names are kept.
4307 // They used to be skipped over on the way to the
4308 // DEFERRED keyword, so a named form silently behaved
4309 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4310 // every deferrable constraint in the transaction.
4311 let mut names: alloc::vec::Vec<alloc::string::String> =
4312 alloc::vec::Vec::new();
4313 if matches!(self.peek(), Token::All) {
4314 self.advance();
4315 } else {
4316 loop {
4317 let mut n = self.expect_ident_like()?;
4318 // A schema-qualified name (`public.fk_a`)
4319 // identifies the same constraint; PG resolves
4320 // it by the trailing segment.
4321 while matches!(self.peek(), Token::Dot) {
4322 self.advance();
4323 n = self.expect_ident_like()?;
4324 }
4325 names.push(n);
4326 if matches!(self.peek(), Token::Comma) {
4327 self.advance();
4328 } else {
4329 break;
4330 }
4331 }
4332 }
4333 let deferred = match self.peek() {
4334 Token::Ident(s) | Token::QuotedIdent(s)
4335 if s.eq_ignore_ascii_case("deferred") =>
4336 {
4337 true
4338 }
4339 Token::Ident(s) | Token::QuotedIdent(s)
4340 if s.eq_ignore_ascii_case("immediate") =>
4341 {
4342 false
4343 }
4344 other => {
4345 return Err(self.err(alloc::format!(
4346 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4347 )));
4348 }
4349 };
4350 self.advance();
4351 return Ok(Statement::SetConstraints { names, deferred });
4352 }
4353 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4354 // { DEFAULT | '<role>' | <ident> }` (mailrs
4355 // round-10 A.1). pg_dump preamble emits the
4356 // `DEFAULT` form to reset session authorization.
4357 //
4358 // v7.39 (round 697) — this said "SPG has no role system so
4359 // this is a strict no-op". SPG has had one since round 58;
4360 // the comment outlived it, and with it the reason a name
4361 // that is not a role was accepted here. It still switches
4362 // no authorization — what it does now is refuse a role
4363 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4364 // AUTHORIZATION` (handled by the RESET parser
4365 // elsewhere). Reference:
4366 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4367 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4368 {
4369 self.advance(); // AUTHORIZATION
4370 match self.peek().clone() {
4371 Token::Default => {
4372 self.advance();
4373 }
4374 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4375 self.advance();
4376 return Ok(Statement::ValidateOnly {
4377 kind: crate::ast::ValidateOnlyKind::RoleName,
4378 names: alloc::vec![r],
4379 });
4380 }
4381 other => {
4382 return Err(self.err(alloc::format!(
4383 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4384 )));
4385 }
4386 }
4387 return Ok(Statement::Empty);
4388 }
4389 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4390 // ISOLATION LEVEL { READ COMMITTED | READ
4391 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4392 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4393 // PG-standard surface. v7.37.8 accepts the syntax
4394 // and tracks the selected level on
4395 // `Engine::current_isolation_level()`; the actual
4396 // MVCC / SSI semantics implementation lands in
4397 // the 轴 4 isolation framework (separate train).
4398 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4399 // internally; SPG behaves the same (effectively
4400 // READ COMMITTED at every level today).
4401 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4402 {
4403 self.advance(); // TRANSACTION
4404 let modes = self.parse_isolation_level_clauses()?;
4405 return Ok(Statement::SetTransaction { modes });
4406 }
4407 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4408 // alias — same accept-as-no-op as SET NAMES.
4409 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4410 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4411 {
4412 self.advance(); // CHARACTER
4413 self.advance(); // SET
4414 if matches!(
4415 self.peek(),
4416 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4417 ) {
4418 self.advance();
4419 }
4420 return Ok(Statement::Empty);
4421 }
4422 // v7.39 (GUC) — PG spells the timezone GUC as two
4423 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4424 // where <value> is a string/ident or the LOCAL /
4425 // DEFAULT keyword (both mean "back to the default").
4426 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4427 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4428 {
4429 self.advance(); // TIME
4430 self.advance(); // ZONE
4431 let value = match self.peek().clone() {
4432 Token::Ident(s)
4433 if s.eq_ignore_ascii_case("local")
4434 || s.eq_ignore_ascii_case("default") =>
4435 {
4436 self.advance();
4437 crate::ast::SetValue::Default
4438 }
4439 Token::Default => {
4440 self.advance();
4441 crate::ast::SetValue::Default
4442 }
4443 _ => self.parse_set_value()?,
4444 };
4445 return Ok(Statement::SetParameter {
4446 name: "timezone".into(),
4447 value,
4448 local: set_local,
4449 });
4450 }
4451 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4452 // MySQL USER-variable assignment: its own per-session
4453 // namespace, an arbitrary expression on the right, and `:=`
4454 // as a second spelling of `=`. It used to fall into the
4455 // session-PARAMETER list below, whose values are literals and
4456 // whose store nothing reads back under a `@` name — so the
4457 // assignment reported success and vanished.
4458 //
4459 // A `@@`-prefixed LHS is a real engine setting and keeps the
4460 // old path.
4461 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4462 return self.parse_set_user_vars();
4463 }
4464 // v7.14.0 — multi-assignment form
4465 // `SET a = 1, b = 2, …`. Single-assignment is the
4466 // 1-element case. Each LHS may be a regular ident
4467 // or a SessionVar (`@VAR` / `@@VAR`).
4468 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4469 loop {
4470 let lhs = match self.peek().clone() {
4471 Token::SessionVar(s) => {
4472 self.advance();
4473 s
4474 }
4475 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4476 other => {
4477 return Err(self.err(format!(
4478 "expected parameter name after SET, got {other:?}"
4479 )));
4480 }
4481 };
4482 // Accept either `=` or the bare `TO` keyword.
4483 match self.peek() {
4484 Token::Eq => {
4485 self.advance();
4486 }
4487 Token::To => {
4488 self.advance();
4489 }
4490 other => {
4491 return Err(self.err(format!(
4492 "expected `=` or TO after SET {lhs}, got {other:?}"
4493 )));
4494 }
4495 }
4496 let mut value = self.parse_set_value()?;
4497 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4498 // `, name TO` continues a MySQL-style multi-assign,
4499 // anything else is a PG list VALUE
4500 // (`SET search_path = myschema, public`) folded into
4501 // one comma-joined string.
4502 while matches!(self.peek(), Token::Comma) {
4503 let is_assign = matches!(
4504 self.tokens.get(self.pos + 1),
4505 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4506 ) && matches!(
4507 self.tokens.get(self.pos + 2),
4508 Some(Token::Eq | Token::To)
4509 );
4510 if is_assign {
4511 break;
4512 }
4513 self.advance(); // comma
4514 let next = self.parse_set_value()?;
4515 let joined = alloc::format!(
4516 "{}, {}",
4517 set_value_text(&value),
4518 set_value_text(&next)
4519 );
4520 value = crate::ast::SetValue::String(joined);
4521 }
4522 pairs.push((lhs, value));
4523 if matches!(self.peek(), Token::Comma) {
4524 self.advance();
4525 continue;
4526 }
4527 break;
4528 }
4529 if pairs.len() == 1 {
4530 let (name, value) = pairs.into_iter().next().unwrap();
4531 Ok(Statement::SetParameter {
4532 name,
4533 value,
4534 local: set_local,
4535 })
4536 } else {
4537 Ok(Statement::SetParameterList(pairs))
4538 }
4539 }
4540 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4541 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4542 self.advance();
4543 match self.peek().clone() {
4544 Token::All => {
4545 self.advance();
4546 Ok(Statement::ResetParameter(None))
4547 }
4548 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4549 self.advance();
4550 Ok(Statement::ResetParameter(None))
4551 }
4552 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4553 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4554 self.advance();
4555 Ok(Statement::SetRole(None))
4556 }
4557 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4558 // (pg_dump's return from the owner switch).
4559 Token::Ident(s) | Token::QuotedIdent(s)
4560 if s.eq_ignore_ascii_case("session")
4561 && matches!(
4562 self.tokens.get(self.pos + 1),
4563 Some(Token::Ident(a) | Token::QuotedIdent(a))
4564 if a.eq_ignore_ascii_case("authorization")
4565 ) =>
4566 {
4567 self.advance(); // SESSION
4568 self.advance(); // AUTHORIZATION
4569 Ok(Statement::SetRole(None))
4570 }
4571 _ => {
4572 let name = self.parse_set_param_name()?;
4573 Ok(Statement::ResetParameter(Some(name)))
4574 }
4575 }
4576 }
4577 // v7.39 (round 218) — server-side cursors.
4578 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4579 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4580 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4581 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4582 self.advance();
4583 match self.peek().clone() {
4584 Token::All => {
4585 self.advance();
4586 Ok(Statement::CloseCursor { name: None })
4587 }
4588 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4589 self.advance();
4590 Ok(Statement::CloseCursor { name: None })
4591 }
4592 Token::Ident(n) | Token::QuotedIdent(n) => {
4593 self.advance();
4594 Ok(Statement::CloseCursor { name: Some(n) })
4595 }
4596 other => Err(self.err(format!(
4597 "expected cursor name or ALL after CLOSE, got {other:?}"
4598 ))),
4599 }
4600 }
4601 other => Err(self.err(format!(
4602 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4603 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4604 ))),
4605 }
4606 }
4607
4608 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4609 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4610 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4611 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4612 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4613 self.advance(); // DECLARE
4614 let name = match self.advance() {
4615 Token::Ident(n) | Token::QuotedIdent(n) => n,
4616 other => {
4617 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4618 }
4619 };
4620 let mut scroll: Option<bool> = None;
4621 loop {
4622 match self.peek() {
4623 Token::Ident(s)
4624 if s.eq_ignore_ascii_case("binary")
4625 || s.eq_ignore_ascii_case("insensitive")
4626 || s.eq_ignore_ascii_case("asensitive") =>
4627 {
4628 self.advance();
4629 }
4630 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4631 self.advance();
4632 scroll = Some(true);
4633 }
4634 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4635 {
4636 self.advance(); // NO
4637 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4638 return Err(self.err(format!(
4639 "expected SCROLL after NO in DECLARE, got {:?}",
4640 self.peek()
4641 )));
4642 }
4643 self.advance();
4644 scroll = Some(false);
4645 }
4646 _ => break,
4647 }
4648 }
4649 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4650 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4651 }
4652 self.advance();
4653 let mut hold = false;
4654 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4655 self.advance();
4656 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4657 return Err(self.err(format!(
4658 "expected HOLD after WITH in DECLARE, got {:?}",
4659 self.peek()
4660 )));
4661 }
4662 self.advance();
4663 hold = true;
4664 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4665 self.advance();
4666 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4667 return Err(self.err(format!(
4668 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4669 self.peek()
4670 )));
4671 }
4672 self.advance();
4673 }
4674 if !matches!(self.peek(), Token::For) {
4675 return Err(self.err(format!(
4676 "expected FOR before the cursor query, got {:?}",
4677 self.peek()
4678 )));
4679 }
4680 self.advance();
4681 let query = self.parse_one_statement()?;
4682 Ok(Statement::DeclareCursor {
4683 name,
4684 scroll,
4685 hold,
4686 query: alloc::boxed::Box::new(query),
4687 })
4688 }
4689
4690 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4691 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4692 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4693 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4694 use crate::ast::CursorDirection as D;
4695 self.advance(); // FETCH / MOVE
4696 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4697 let neg = if matches!(this.peek(), Token::Minus) {
4698 this.advance();
4699 true
4700 } else {
4701 false
4702 };
4703 match this.advance() {
4704 Token::Integer(v) => Ok(if neg { -v } else { v }),
4705 other => Err(this.err(format!("expected count, got {other:?}"))),
4706 }
4707 };
4708 let direction = match self.peek().clone() {
4709 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4710 self.advance();
4711 D::Next
4712 }
4713 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4714 self.advance();
4715 D::Prior
4716 }
4717 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4718 self.advance();
4719 D::First
4720 }
4721 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4722 self.advance();
4723 D::Last
4724 }
4725 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4726 self.advance();
4727 D::Absolute(signed_count(self)?)
4728 }
4729 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4730 self.advance();
4731 D::Relative(signed_count(self)?)
4732 }
4733 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4734 self.advance();
4735 match self.peek().clone() {
4736 Token::All => {
4737 self.advance();
4738 D::All
4739 }
4740 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4741 self.advance();
4742 D::All
4743 }
4744 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4745 _ => D::Next, // bare FORWARD = FORWARD 1
4746 }
4747 }
4748 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4749 self.advance();
4750 match self.peek().clone() {
4751 Token::All => {
4752 self.advance();
4753 D::BackwardAll
4754 }
4755 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4756 self.advance();
4757 D::BackwardAll
4758 }
4759 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4760 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4761 }
4762 }
4763 Token::All => {
4764 self.advance();
4765 D::All
4766 }
4767 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4768 self.advance();
4769 D::All
4770 }
4771 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4772 // Bare `FETCH <name>` — direction defaults to NEXT.
4773 _ => D::Next,
4774 };
4775 // Optional FROM / IN.
4776 if matches!(self.peek(), Token::From)
4777 || matches!(self.peek(), Token::In)
4778 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4779 {
4780 self.advance();
4781 }
4782 let name = match self.advance() {
4783 Token::Ident(n) | Token::QuotedIdent(n) => n,
4784 other => {
4785 return Err(self.err(format!("expected cursor name, got {other:?}")));
4786 }
4787 };
4788 Ok(if is_move {
4789 Statement::MoveCursor { name, direction }
4790 } else {
4791 Statement::FetchCursor { name, direction }
4792 })
4793 }
4794
4795 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4796 /// [(kind, …)] ON <col>, … FROM <table>`.
4797 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4798 self.advance(); // STATISTICS
4799 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4800 let mut if_not_exists = false;
4801 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4802 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4803 {
4804 self.advance();
4805 self.advance();
4806 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4807 self.advance();
4808 if_not_exists = true;
4809 }
4810 }
4811 let name = self.expect_ident_like()?;
4812 let mut kinds = Vec::new();
4813 if matches!(self.peek(), Token::LParen) {
4814 self.advance();
4815 loop {
4816 let k = self.expect_ident_like()?;
4817 // PG stores the single letters; accept the spelled-out
4818 // names the SQL uses and record what PG records.
4819 kinds.push(match k.to_ascii_lowercase().as_str() {
4820 "ndistinct" => String::from("d"),
4821 "dependencies" => String::from("f"),
4822 "mcv" => String::from("m"),
4823 other => {
4824 return Err(
4825 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4826 );
4827 }
4828 });
4829 match self.advance() {
4830 Token::Comma => {}
4831 Token::RParen => break,
4832 other => {
4833 return Err(self.err(alloc::format!(
4834 "expected ',' or ')' in statistics kind list, got {other:?}"
4835 )));
4836 }
4837 }
4838 }
4839 }
4840 if !matches!(self.peek(), Token::On) {
4841 return Err(self.err(alloc::format!(
4842 "expected ON in CREATE STATISTICS, got {:?}",
4843 self.peek()
4844 )));
4845 }
4846 self.advance();
4847 let mut columns = Vec::new();
4848 loop {
4849 columns.push(self.expect_ident_like()?);
4850 if matches!(self.peek(), Token::Comma) {
4851 self.advance();
4852 } else {
4853 break;
4854 }
4855 }
4856 if !matches!(self.peek(), Token::From) {
4857 return Err(self.err(alloc::format!(
4858 "expected FROM in CREATE STATISTICS, got {:?}",
4859 self.peek()
4860 )));
4861 }
4862 self.advance();
4863 let table = self.expect_ident_like()?;
4864 Ok(Statement::CreateStatistics {
4865 name,
4866 if_not_exists,
4867 kinds,
4868 columns,
4869 table,
4870 })
4871 }
4872
4873 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4874 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4875 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4876 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4877 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4878 /// forward call.
4879 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4880 self.advance(); // TABLE
4881 let if_exists = self.consume_if_exists();
4882 let mut names: Vec<String> = Vec::new();
4883 loop {
4884 names.push(self.expect_ident_like()?);
4885 if matches!(self.peek(), Token::Comma) {
4886 self.advance();
4887 continue;
4888 }
4889 break;
4890 }
4891 if matches!(
4892 self.peek(),
4893 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4894 || s.eq_ignore_ascii_case("restrict")
4895 ) {
4896 self.advance();
4897 }
4898 Ok(Statement::DropTable { names, if_exists })
4899 }
4900
4901 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4902 self.advance(); // STATISTICS
4903 let mut if_exists = false;
4904 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4905 && matches!(self.tokens.get(self.pos + 1),
4906 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4907 {
4908 self.advance();
4909 self.advance();
4910 if_exists = true;
4911 }
4912 let name = self.expect_ident_like()?;
4913 Ok(Statement::DropStatistics { name, if_exists })
4914 }
4915
4916 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4917 debug_assert!(matches!(self.peek(), Token::Create));
4918 self.advance();
4919 match self.peek() {
4920 Token::Table => self.parse_create_table_stmt_after_create(),
4921 Token::Index => self.parse_create_index_stmt_after_create(false),
4922 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4923 // object now. It used to be consumed by the CREATE-noise
4924 // arm, so a pg_dump that declares extended statistics
4925 // restored silently without them and reflection showed
4926 // nothing.
4927 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4928 self.parse_create_statistics_after_create()
4929 }
4930 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4931 // The `UNIQUE` modifier turns a partial index into a
4932 // partial-uniqueness invariant (only rows matching the
4933 // WHERE predicate are checked for duplicates). mailrs
4934 // K1 (3 hits: email_templates default, calendar_events
4935 // master, calendar_events instance).
4936 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4937 self.advance();
4938 if !matches!(self.peek(), Token::Index) {
4939 return Err(self.err(alloc::format!(
4940 "expected INDEX after CREATE UNIQUE, got {:?}",
4941 self.peek()
4942 )));
4943 }
4944 self.parse_create_index_stmt_after_create(true)
4945 }
4946 Token::Publication => {
4947 self.advance();
4948 self.parse_create_publication_after_keyword()
4949 }
4950 Token::Subscription => {
4951 self.advance();
4952 self.parse_create_subscription_after_keyword()
4953 }
4954 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4955 // USER isn't a reserved keyword — we look for the bare
4956 // identifier so the lexer doesn't have to grow a token.
4957 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4958 self.advance();
4959 self.parse_create_user_after_keyword(true)
4960 }
4961 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4962 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4963 // the default of the LOGIN attribute.
4964 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4965 self.advance();
4966 self.parse_create_user_after_keyword(false)
4967 }
4968 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4969 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4970 self.advance();
4971 self.parse_create_policy_after_keyword()
4972 }
4973 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4974 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4975 // no-op. mailrs follow-up F3.
4976 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4977 self.advance();
4978 self.parse_create_extension_after_keyword()
4979 }
4980 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4981 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4982 // optional; absorb it here and forward to the
4983 // per-kind parsers with the flag. OR is a reserved
4984 // keyword token.
4985 Token::Or => {
4986 self.advance();
4987 let next = self.peek();
4988 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4989 return Err(self.err(alloc::format!(
4990 "expected REPLACE after CREATE OR, got {next:?}"
4991 )));
4992 };
4993 if !s2.eq_ignore_ascii_case("replace") {
4994 return Err(self.err(alloc::format!(
4995 "expected REPLACE after CREATE OR, got {s2:?}"
4996 )));
4997 }
4998 self.advance();
4999 self.parse_create_function_or_trigger_after_or_replace(true)
5000 }
5001 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
5002 self.advance();
5003 self.parse_create_function_after_keyword(false)
5004 }
5005 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
5006 self.advance();
5007 self.parse_create_trigger_after_keyword(false)
5008 }
5009 // v7.39 (round 139) — CREATE RULE …
5010 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
5011 self.advance();
5012 self.parse_create_rule_after_keyword(false)
5013 }
5014 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
5015 // trigger is a row-level AFTER trigger that additionally carries
5016 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
5017 // path already tolerates and skips those clauses, so consuming the
5018 // CONSTRAINT keyword and reusing it makes the statement parse and the
5019 // trigger fire. (The deferral timing itself is not yet honoured —
5020 // SPG fires it as a plain AFTER trigger, which is correct behaviour
5021 // for every non-deferred use.)
5022 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5023 self.advance();
5024 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5025 if t.eq_ignore_ascii_case("trigger"))
5026 {
5027 return Err(self.err(alloc::format!(
5028 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5029 self.peek()
5030 )));
5031 }
5032 self.advance();
5033 self.parse_create_trigger_after_keyword(false)
5034 }
5035 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5036 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5037 self.advance();
5038 self.parse_create_sequence_after_keyword(false)
5039 }
5040 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5041 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5042 self.advance();
5043 self.parse_create_view_after_keyword(false, false, false)
5044 }
5045 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5046 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5047 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5048 // appear (in any order) between `CREATE` and `VIEW` in
5049 // every mysqldump-emitted view. Pre-2.6 the parser
5050 // rejected the prefix and the customer's whole view
5051 // backup failed on the first view. The hints are pure
5052 // planner / permission metadata; SPG's view-rewrite
5053 // path is semantically equivalent for all three
5054 // algorithms in v7.17 (TEMPTABLE differs only in
5055 // perf for huge views — out of v7.17 scope), and
5056 // DEFINER / SQL SECURITY are pure single-user
5057 // permissioning that SPG ignores by design.
5058 Token::Ident(s) | Token::QuotedIdent(s)
5059 if s.eq_ignore_ascii_case("algorithm")
5060 || s.eq_ignore_ascii_case("definer")
5061 || s.eq_ignore_ascii_case("sql") =>
5062 {
5063 self.consume_mysql_view_prefix()?;
5064 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5065 // (in any order, in any combination), the next
5066 // keyword must be VIEW. mysqldump never emits these
5067 // prefixes on non-view statements.
5068 let next = self.peek().clone();
5069 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5070 if s2.eq_ignore_ascii_case("view"))
5071 {
5072 self.advance();
5073 self.parse_create_view_after_keyword(false, false, false)
5074 } else {
5075 Err(self.err(alloc::format!(
5076 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5077 )))
5078 }
5079 }
5080 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5081 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5082 self.advance();
5083 self.parse_create_type_after_keyword()
5084 }
5085 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5086 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5087 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5088 self.advance();
5089 self.parse_create_domain_after_keyword()
5090 }
5091 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5092 // name [AUTHORIZATION user]. Real catalog registry
5093 // (was silent-no-op'd pre-v7.17).
5094 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5095 self.advance();
5096 let if_not_exists = self.parse_if_not_exists();
5097 let name = self.expect_ident_like()?;
5098 // Optional `AUTHORIZATION <user>` trailer — accepted,
5099 // ignored (single-user catalog).
5100 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5101 if s.eq_ignore_ascii_case("authorization"))
5102 {
5103 self.advance();
5104 let _ = self.expect_ident_like()?;
5105 }
5106 Ok(Statement::CreateSchema { name, if_not_exists })
5107 }
5108 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5109 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5110 self.advance();
5111 let next = self.peek().clone();
5112 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5113 {
5114 self.advance();
5115 self.parse_create_materialized_view_after_keyword()
5116 } else {
5117 Err(self.err(alloc::format!(
5118 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5119 )))
5120 }
5121 }
5122 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5123 // no-op below), an UNLOGGED table is a real, fully-usable table in
5124 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5125 // durability optimisation is a follow-up), so a dump / app that
5126 // declares UNLOGGED tables works instead of failing to parse.
5127 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5128 self.advance(); // UNLOGGED
5129 if matches!(self.peek(), Token::Table) {
5130 self.parse_create_table_stmt_after_create()
5131 } else {
5132 Err(self.err(format!(
5133 "expected TABLE after CREATE UNLOGGED, got {:?}",
5134 self.peek()
5135 )))
5136 }
5137 }
5138 Token::Ident(s) | Token::QuotedIdent(s)
5139 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5140 {
5141 self.advance();
5142 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5143 let next = self.peek().clone();
5144 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5145 {
5146 self.advance();
5147 self.parse_create_sequence_after_keyword(true)
5148 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5149 {
5150 self.advance();
5151 self.parse_create_view_after_keyword(false, false, true)
5152 } else {
5153 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5154 // consumed and answered OK while creating nothing, so
5155 // every statement that touched the table afterwards failed
5156 // with "table not found" — the DDL itself lied. It is a
5157 // real CREATE TABLE now, marked temporary so the executor
5158 // puts it in the session's own namespace. An optional
5159 // TABLE keyword may or may not be present (`CREATE TEMP t`
5160 // is not legal, but the keyword is consumed by the
5161 // CREATE TABLE parser itself).
5162 let stmt = self.parse_create_table_stmt_after_create()?;
5163 match stmt {
5164 Statement::CreateTable(mut c) => {
5165 c.temporary = true;
5166 Ok(Statement::CreateTable(c))
5167 }
5168 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5169 // CTAS node, which needs the same session namespace.
5170 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5171 m.temporary = true;
5172 Ok(Statement::CreateMaterializedView(m))
5173 }
5174 other => Ok(other),
5175 }
5176 }
5177 }
5178 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5179 // BEGIN <body> END`. The body may reference `@var`
5180 // session variables, SET statements, internal `;`
5181 // terminators, etc. SPG has no procedure runtime, so
5182 // consume the whole `CREATE PROCEDURE … END` block as
5183 // a no-op so mysqldump scripts that include stored
5184 // routines load through. The matching-END consumer
5185 // tracks BEGIN/END nesting depth to handle nested
5186 // BEGIN blocks correctly.
5187 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5188 self.consume_mysql_routine_body();
5189 Ok(Statement::Empty)
5190 }
5191 // v7.14.0 — pg_dump / mysqldump emit
5192 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5193 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5194 // SPG is single-schema / single-database; these have
5195 // no behavioural effect, so consume + return Empty.
5196 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5197 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5198 // moved up to real parser branches. DATABASE / ROLE /
5199 // POLICY / OPERATOR stay no-op forever
5200 // (single-database, hardcoded roles).
5201 Token::Ident(s) | Token::QuotedIdent(s)
5202 if matches!(
5203 s.to_ascii_lowercase().as_str(),
5204 "database"
5205 | "role"
5206 | "operator"
5207 | "cast"
5208 | "aggregate"
5209 | "language"
5210 | "collation"
5211 | "conversion"
5212 // v7.17.0 Phase 8 (audit N6) — rarely-
5213 // emitted pg_dump shapes that should
5214 // load through without a parser error.
5215 // SPG has no planner statistics catalog,
5216 // no event-trigger hooks, no foreign-
5217 // data-wrapper infrastructure; consume
5218 // + return Empty.
5219 | "statistics"
5220 | "event"
5221 // v7.37.17 (17.6 siblings) — additional CREATE
5222 // targets pg_dump / operator install scripts
5223 // may emit that SPG has no matching machinery
5224 // for. Consume + Empty-return.
5225 | "text"
5226 | "tablespace"
5227 | "access"
5228 | "large"
5229 ) =>
5230 {
5231 // DATABASE is the one member of this list PG refuses
5232 // inside a transaction block; the rest (ROLE, CAST,
5233 // TABLESPACE, …) it runs there quite happily, so only
5234 // this one is named. Still a no-op otherwise — SPG is
5235 // single-database.
5236 let is_database = s.eq_ignore_ascii_case("database");
5237 // The name is the first token after DATABASE, past an
5238 // `IF NOT EXISTS`.
5239 let name = if is_database {
5240 self.scan_database_name()
5241 } else {
5242 None
5243 };
5244 let collation = if is_database {
5245 self.scan_database_collation_until_boundary()
5246 } else {
5247 self.consume_until_statement_boundary();
5248 None
5249 };
5250 if is_database {
5251 return Ok(Statement::NoOpPreventedInTransaction {
5252 what: String::from("CREATE DATABASE"),
5253 collation,
5254 name,
5255 });
5256 }
5257 Ok(Statement::Empty)
5258 }
5259 // v7.39 (round 706) — the foreign-data family leaves the silent
5260 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5261 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5262 // FDW machinery), but the ENGINE now warns, so a restore log
5263 // says what will not function instead of reporting success.
5264 Token::Ident(s) | Token::QuotedIdent(s)
5265 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5266 {
5267 self.consume_until_statement_boundary();
5268 Ok(Statement::ValidateOnly {
5269 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5270 names: Vec::new(),
5271 })
5272 }
5273 other => Err(self.err(format!(
5274 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5275 ))),
5276 }
5277 }
5278
5279 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5280 /// keyword decides whether we parse a function or trigger
5281 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5282 /// PROCEDURE) — those land in later releases.
5283 fn parse_create_function_or_trigger_after_or_replace(
5284 &mut self,
5285 or_replace: bool,
5286 ) -> Result<Statement, ParseError> {
5287 let tok = self.peek();
5288 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5289 return Err(self.err(alloc::format!(
5290 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5291 )));
5292 };
5293 if s.eq_ignore_ascii_case("function") {
5294 self.advance();
5295 self.parse_create_function_after_keyword(or_replace)
5296 } else if s.eq_ignore_ascii_case("trigger") {
5297 self.advance();
5298 self.parse_create_trigger_after_keyword(or_replace)
5299 } else if s.eq_ignore_ascii_case("rule") {
5300 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5301 self.advance();
5302 self.parse_create_rule_after_keyword(or_replace)
5303 } else if s.eq_ignore_ascii_case("view") {
5304 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5305 self.advance();
5306 self.parse_create_view_after_keyword(or_replace, false, false)
5307 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5308 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5309 self.advance();
5310 let nxt = self.peek().clone();
5311 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5312 {
5313 self.advance();
5314 self.parse_create_view_after_keyword(or_replace, false, true)
5315 } else {
5316 Err(self.err(alloc::format!(
5317 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5318 )))
5319 }
5320 } else {
5321 Err(self.err(alloc::format!(
5322 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5323 )))
5324 }
5325 }
5326
5327 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5328 /// SPG doesn't have a registry; pgvector / similar are
5329 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5330 /// the syntax lets dual-target schemas keep the line.
5331 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5332 // Optional `IF NOT EXISTS`.
5333 self.consume_if_not_exists();
5334 let name = self.expect_ident_like()?;
5335 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5336 // CASCADE / FROM '<v>' clauses; we don't model them.
5337 loop {
5338 match self.peek() {
5339 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5340 self.advance();
5341 continue;
5342 }
5343 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5344 self.advance();
5345 let _ = self.expect_ident_like()?;
5346 continue;
5347 }
5348 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5349 self.advance();
5350 // String or ident literal.
5351 let _ = self.advance();
5352 continue;
5353 }
5354 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5355 self.advance();
5356 let _ = self.advance();
5357 continue;
5358 }
5359 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5360 self.advance();
5361 continue;
5362 }
5363 _ => break,
5364 }
5365 }
5366 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5367 // nosuch` reported success and `pg_extension` then did not list it,
5368 // which is the accept-and-do-nothing shape F31 exists to find.
5369 Ok(Statement::ValidateOnly {
5370 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5371 names: alloc::vec![name],
5372 })
5373 }
5374
5375 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5376 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5377 /// already been consumed by the caller. Grammar accepted:
5378 ///
5379 /// name `(` arg-list `)`
5380 /// `RETURNS` return-type
5381 /// [ `LANGUAGE` ident ]
5382 /// `AS` $$ body $$
5383 /// [ `LANGUAGE` ident ]
5384 ///
5385 /// Either `LANGUAGE` position is allowed; PG accepts both.
5386 fn parse_create_function_after_keyword(
5387 &mut self,
5388 or_replace: bool,
5389 ) -> Result<Statement, ParseError> {
5390 let name = self.expect_ident_like()?;
5391 // Argument list. v7.12.4 commonly sees the empty `()`
5392 // (trigger functions); typed args parse and round-trip
5393 // but the executor only invokes nullary functions.
5394 if !matches!(self.peek(), Token::LParen) {
5395 return Err(self.err(alloc::format!(
5396 "expected '(' after function name {name:?}, got {:?}",
5397 self.peek()
5398 )));
5399 }
5400 self.advance();
5401 let args = self.parse_function_arg_list()?;
5402 // RETURNS clause.
5403 let tok = self.peek();
5404 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5405 return Err(self.err(alloc::format!(
5406 "expected RETURNS after function arg list, got {tok:?}"
5407 )));
5408 };
5409 if !s.eq_ignore_ascii_case("returns") {
5410 return Err(self.err(alloc::format!(
5411 "expected RETURNS after function arg list, got {s:?}"
5412 )));
5413 }
5414 self.advance();
5415 let returns = self.parse_function_return()?;
5416 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5417 // re-check after the body too).
5418 let mut language: Option<String> = self.parse_optional_language()?;
5419 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5420 // either side of the body and in any order, interleaved with
5421 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5422 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5423 // PG's own pg_dump output did not restore.
5424 let mut attrs = FunctionAttrs::default();
5425 loop {
5426 let before = self.pos;
5427 self.parse_function_attrs_into(&mut attrs)?;
5428 if language.is_none() {
5429 language = self.parse_optional_language()?;
5430 }
5431 if self.pos == before {
5432 break;
5433 }
5434 }
5435 // `AS` followed by a $$-quoted body (lexer already
5436 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5437 // Token::String). AS is a reserved keyword (Token::As).
5438 if !matches!(self.peek(), Token::As) {
5439 return Err(self.err(alloc::format!(
5440 "expected AS before function body, got {:?}",
5441 self.peek()
5442 )));
5443 }
5444 self.advance();
5445 let body_text = match self.peek() {
5446 Token::String(s) => {
5447 let body = s.clone();
5448 self.advance();
5449 body
5450 }
5451 other => {
5452 return Err(self.err(alloc::format!(
5453 "expected $$-quoted function body after AS, got {other:?}"
5454 )));
5455 }
5456 };
5457 // Trailing clauses — PG's other accepted position for both the
5458 // LANGUAGE and the attributes.
5459 loop {
5460 let before = self.pos;
5461 self.parse_function_attrs_into(&mut attrs)?;
5462 if language.is_none() {
5463 language = self.parse_optional_language()?;
5464 }
5465 if self.pos == before {
5466 break;
5467 }
5468 }
5469 let language = language.unwrap_or_else(|| String::from("sql"));
5470 // PL/pgSQL bodies get structure-parsed. Other languages
5471 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5472 // recognise) round-trip as Raw text — the executor errors
5473 // when invoked with a clear unsupported message.
5474 let body = if language.eq_ignore_ascii_case("plpgsql") {
5475 match parse_plpgsql_body(&body_text) {
5476 Ok(block) => FunctionBody::PlPgSql(block),
5477 // Best-effort: if the body parser doesn't yet
5478 // support a construct used inside, fall back to
5479 // raw — keeps `CREATE FUNCTION` itself working
5480 // (catalogue accepts), executor errors on
5481 // invocation only.
5482 Err(_) => FunctionBody::Raw(body_text),
5483 }
5484 } else {
5485 FunctionBody::Raw(body_text)
5486 };
5487 Ok(Statement::CreateFunction(CreateFunctionStatement {
5488 name,
5489 or_replace,
5490 args,
5491 returns,
5492 language,
5493 body,
5494 attrs,
5495 }))
5496 }
5497
5498 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5499 /// attribute clauses into `attrs`, stopping at the first token that
5500 /// is not one. Measured against PG 18.4, which accepts them in any
5501 /// order and on either side of the body.
5502 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5503 loop {
5504 let word = match self.peek() {
5505 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5506 // NOT LEAKPROOF — NOT is a reserved keyword token.
5507 Token::Not
5508 if matches!(
5509 self.tokens.get(self.pos + 1),
5510 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5511 ) =>
5512 {
5513 self.advance();
5514 self.advance();
5515 attrs.leakproof = false;
5516 continue;
5517 }
5518 _ => return Ok(()),
5519 };
5520 match word.as_str() {
5521 "immutable" => {
5522 self.advance();
5523 attrs.volatility = FunctionVolatility::Immutable;
5524 }
5525 "stable" => {
5526 self.advance();
5527 attrs.volatility = FunctionVolatility::Stable;
5528 }
5529 "volatile" => {
5530 self.advance();
5531 attrs.volatility = FunctionVolatility::Volatile;
5532 }
5533 "strict" => {
5534 self.advance();
5535 attrs.strict = true;
5536 }
5537 "leakproof" => {
5538 self.advance();
5539 attrs.leakproof = true;
5540 }
5541 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5542 // spelled-out forms of STRICT and its opposite.
5543 "returns" | "called" => {
5544 let strict = word == "returns";
5545 let mut probe = self.pos + 1;
5546 if strict {
5547 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5548 // is not ours.
5549 match self.tokens.get(probe) {
5550 Some(Token::Null) => probe += 1,
5551 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5552 _ => return Ok(()),
5553 }
5554 }
5555 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5556 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5557 if !ok {
5558 return Ok(());
5559 }
5560 probe += 1;
5561 match self.tokens.get(probe) {
5562 Some(Token::Null) => probe += 1,
5563 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5564 _ => return Ok(()),
5565 }
5566 match self.tokens.get(probe) {
5567 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5568 _ => return Ok(()),
5569 }
5570 self.pos = probe;
5571 attrs.strict = strict;
5572 }
5573 "security" | "external" => {
5574 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5575 let mut probe = self.pos + 1;
5576 if word == "external" {
5577 match self.tokens.get(probe) {
5578 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5579 probe += 1;
5580 }
5581 _ => return Ok(()),
5582 }
5583 }
5584 let definer = match self.tokens.get(probe) {
5585 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5586 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5587 _ => return Ok(()),
5588 };
5589 self.pos = probe + 1;
5590 attrs.security_definer = definer;
5591 }
5592 "parallel" => {
5593 let level = match self.tokens.get(self.pos + 1) {
5594 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5595 FunctionParallel::Safe
5596 }
5597 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5598 FunctionParallel::Restricted
5599 }
5600 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5601 FunctionParallel::Unsafe
5602 }
5603 _ => return Ok(()),
5604 };
5605 self.pos += 2;
5606 attrs.parallel = level;
5607 }
5608 "cost" | "rows" => {
5609 let Some(n) = self.peek_number_at(self.pos + 1) else {
5610 return Ok(());
5611 };
5612 self.pos += 2;
5613 if word == "cost" {
5614 attrs.cost = Some(n);
5615 } else {
5616 attrs.rows = Some(n);
5617 }
5618 }
5619 _ => return Ok(()),
5620 }
5621 }
5622 }
5623
5624 /// The numeric literal at `idx`, if there is one.
5625 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5626 match self.tokens.get(idx)? {
5627 Token::Integer(n) => Some(*n as f64),
5628 Token::Float(f) => Some(*f),
5629 Token::Numeric(t) => t.parse::<f64>().ok(),
5630 _ => None,
5631 }
5632 }
5633
5634 /// Closing `)`-terminated argument list. v7.12.4 commonly
5635 /// sees the empty `()`; typed args round-trip but the
5636 /// executor (yet) doesn't invoke them.
5637 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5638 /// it away, which is what PG does with one on a function parameter.
5639 fn skip_type_modifier(&mut self) {
5640 if !matches!(self.peek(), Token::LParen) {
5641 return;
5642 }
5643 // Only a numeric modifier — anything else is not one, and eating
5644 // it would swallow real grammar.
5645 let mut i = self.pos + 1;
5646 let mut seen_number = false;
5647 loop {
5648 match self.tokens.get(i) {
5649 Some(Token::Integer(_)) => seen_number = true,
5650 Some(Token::Comma) => {}
5651 Some(Token::RParen) => break,
5652 _ => return,
5653 }
5654 i += 1;
5655 }
5656 if !seen_number {
5657 return;
5658 }
5659 while self.pos <= i {
5660 self.advance();
5661 }
5662 }
5663
5664 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5665 let mut args: Vec<FunctionArg> = Vec::new();
5666 if matches!(self.peek(), Token::RParen) {
5667 self.advance();
5668 return Ok(args);
5669 }
5670 loop {
5671 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5672 // a reserved token; OUT / INOUT are bare idents.
5673 let mode = if matches!(self.peek(), Token::In) {
5674 self.advance();
5675 FunctionArgMode::In
5676 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5677 {
5678 self.advance();
5679 FunctionArgMode::Out
5680 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5681 {
5682 self.advance();
5683 FunctionArgMode::InOut
5684 } else {
5685 FunctionArgMode::In
5686 };
5687 // Optional name. The next token is either a name
5688 // (followed by a type ident) or the type itself.
5689 // Disambiguate by peeking ahead: if the token after
5690 // the next ident is also an ident, we treat the
5691 // first as the name.
5692 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5693 // the comma or paren, then decide. Reading at most two of
5694 // them could not spell `x double precision` at all, and
5695 // silently mis-read the bare `double precision` as a
5696 // parameter named "double" — which is what made the same
5697 // signature key two different ways.
5698 let (name, ty_token) = {
5699 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5700 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5701 words.push(self.expect_ident_like()?);
5702 }
5703 // v7.39 (round 344) — a length / precision modifier on the
5704 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5705 // accepts it and DROPS it — `pg_get_function_arguments`
5706 // reports plain `character varying` / `numeric`, measured on
5707 // 18.4 — but SPG raised `syntax error at or near "("`,
5708 // because the modifier's parens were never consumed.
5709 self.skip_type_modifier();
5710 // r1049 — `f(v bigint[])`. The array suffix parsed in
5711 // the column position, the cast position and (r1038)
5712 // the RETURNS position, but not here: the fifth
5713 // member of the same family, reported by sentori as
5714 // presumably the same code. It is now.
5715 let array_suffix = self.consume_array_suffix();
5716 let whole = words.join(" ");
5717 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5718 {
5719 (Some(words[0].clone()), words[1..].join(" "))
5720 } else {
5721 (None, whole)
5722 };
5723 ty_token.push_str(&array_suffix);
5724 (name, ty_token)
5725 };
5726 // Type — try to map to ColumnTypeName, else Raw.
5727 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5728 Some(t) => FunctionArgType::Typed(t),
5729 None => FunctionArgType::Raw(ty_token),
5730 };
5731 args.push(FunctionArg { mode, name, ty });
5732 match self.peek() {
5733 Token::Comma => {
5734 self.advance();
5735 continue;
5736 }
5737 Token::RParen => {
5738 self.advance();
5739 return Ok(args);
5740 }
5741 other => {
5742 return Err(self.err(alloc::format!(
5743 "expected , or ) in function arg list, got {other:?}"
5744 )));
5745 }
5746 }
5747 }
5748 }
5749
5750 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5751 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5752 // function whose row shape is named inline.
5753 if matches!(self.peek(), Token::Table)
5754 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5755 {
5756 self.advance(); // TABLE
5757 self.advance(); // (
5758 let mut cols: Vec<String> = Vec::new();
5759 loop {
5760 let cname = self.expect_ident_like()?;
5761 let mut ty: Vec<String> = Vec::new();
5762 loop {
5763 match self.peek() {
5764 Token::Comma | Token::RParen | Token::Eof => break,
5765 _ => {}
5766 }
5767 match self.advance() {
5768 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5769 other => {
5770 if let Some(w) = unreserved_keyword_text(&other) {
5771 ty.push(w);
5772 }
5773 }
5774 }
5775 }
5776 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5777 if matches!(self.peek(), Token::Comma) {
5778 self.advance();
5779 } else {
5780 break;
5781 }
5782 }
5783 if matches!(self.peek(), Token::RParen) {
5784 self.advance();
5785 }
5786 return Ok(FunctionReturn::Other(alloc::format!(
5787 "TABLE({})",
5788 cols.join(", ")
5789 )));
5790 }
5791 let ident = self.expect_ident_like()?;
5792 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5793 if ident.eq_ignore_ascii_case("setof") {
5794 let inner = self.expect_ident_like()?;
5795 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5796 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5797 }
5798 if ident.eq_ignore_ascii_case("trigger") {
5799 return Ok(FunctionReturn::Trigger);
5800 }
5801 if ident.eq_ignore_ascii_case("void") {
5802 return Ok(FunctionReturn::Void);
5803 }
5804 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5805 // RETURN position did not, so the `[` was a syntax error and the
5806 // whole migration stopped. sentori worked around it by returning
5807 // zero-padded text.
5808 let suffix = self.consume_array_suffix();
5809 if !suffix.is_empty() {
5810 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5811 }
5812 match map_type_ident_to_column_type_name(&ident) {
5813 Some(t) => Ok(FunctionReturn::Type(t)),
5814 None => Ok(FunctionReturn::Other(ident)),
5815 }
5816 }
5817
5818 /// Consume any `[]` / `[N]` array markers after a type name and give
5819 /// back their text. Empty when there are none.
5820 fn consume_array_suffix(&mut self) -> String {
5821 let mut out = String::new();
5822 while matches!(self.peek(), Token::LBracket) {
5823 self.advance();
5824 // `[N]` is accepted and, as in PG, the length is not enforced.
5825 if let Token::Integer(n) = self.peek().clone() {
5826 self.advance();
5827 out.push_str(&alloc::format!("[{n}]"));
5828 } else {
5829 out.push_str("[]");
5830 }
5831 if matches!(self.peek(), Token::RBracket) {
5832 self.advance();
5833 }
5834 }
5835 out
5836 }
5837
5838 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5839 match self.peek() {
5840 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5841 self.advance();
5842 let lang = self.expect_ident_like()?;
5843 Ok(Some(lang.to_ascii_lowercase()))
5844 }
5845 _ => Ok(None),
5846 }
5847 }
5848
5849 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5850 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5851 /// (expr)]*`. The `DOMAIN` keyword has already been
5852 /// consumed. PG allows the trailing constraints in any
5853 /// order; we approximate with a small loop.
5854 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5855 let name = self.expect_ident_like()?;
5856 // Optional `AS`.
5857 if matches!(self.peek(), Token::As) {
5858 self.advance();
5859 }
5860 // v7.39 (round 259) — keep the raw type NAME when the base is not
5861 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5862 // parent domain.
5863 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5864 self.parse_type_with_implied_flags()?;
5865 let mut default: Option<Expr> = None;
5866 let mut not_null = false;
5867 let mut checks: Vec<Expr> = Vec::new();
5868 loop {
5869 match self.peek() {
5870 Token::Default => {
5871 if default.is_some() {
5872 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5873 }
5874 self.advance();
5875 default = Some(self.parse_expr(0)?);
5876 }
5877 Token::Not => {
5878 self.advance();
5879 if !matches!(self.peek(), Token::Null) {
5880 return Err(self.err(alloc::format!(
5881 "expected NULL after NOT in DOMAIN, got {:?}",
5882 self.peek()
5883 )));
5884 }
5885 self.advance();
5886 not_null = true;
5887 }
5888 Token::Null => {
5889 self.advance();
5890 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5891 // is the default-nullable marker (PG accepts it),
5892 // but AFTER a NOT NULL it is a conflict PG refuses
5893 // (`conflicting NULL/NOT NULL constraints`,
5894 // PG18-measured); the old arm no-opped both ways.
5895 if not_null {
5896 return Err(self.err(alloc::string::String::from(
5897 "conflicting NULL/NOT NULL constraints",
5898 )));
5899 }
5900 }
5901 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5902 self.advance();
5903 if !matches!(self.peek(), Token::LParen) {
5904 return Err(self.err(alloc::format!(
5905 "expected '(' after CHECK in DOMAIN, got {:?}",
5906 self.peek()
5907 )));
5908 }
5909 self.advance();
5910 let expr = self.parse_expr(0)?;
5911 if !matches!(self.peek(), Token::RParen) {
5912 return Err(self.err(alloc::format!(
5913 "expected ')' after CHECK expr, got {:?}",
5914 self.peek()
5915 )));
5916 }
5917 self.advance();
5918 checks.push(expr);
5919 }
5920 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5921 // prefix on the constraint; we drop the name and
5922 // recurse into the constraint parsing.
5923 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5924 self.advance();
5925 let _ = self.expect_ident_like()?;
5926 }
5927 _ => break,
5928 }
5929 }
5930 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5931 name,
5932 base_type,
5933 base_domain: base_user_ref,
5934 default,
5935 not_null,
5936 checks,
5937 }))
5938 }
5939
5940 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5941 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5942 /// consumed.
5943 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5944 let name = self.expect_ident_like()?;
5945 // Required `AS`.
5946 if !matches!(self.peek(), Token::As) {
5947 return Err(self.err(alloc::format!(
5948 "expected AS after CREATE TYPE {name:?}, got {:?}",
5949 self.peek()
5950 )));
5951 }
5952 self.advance();
5953 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5954 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5955 // on the next token: `(` = composite, ident `ENUM` = enum.
5956 if matches!(self.peek(), Token::LParen) {
5957 self.advance();
5958 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5959 let mut field_user_types: Vec<Option<String>> = Vec::new();
5960 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5961 // is legal PG (an attribute-less composite; measured — the old
5962 // e2e note claimed PG requires at least one attribute).
5963 if matches!(self.peek(), Token::RParen) {
5964 self.advance();
5965 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5966 name,
5967 kind: crate::ast::TypeKind::Composite {
5968 fields,
5969 field_user_types,
5970 },
5971 }));
5972 }
5973 loop {
5974 let field_name = self.expect_ident_like()?;
5975 // v7.39 (round 264) — keep the raw type name when it is not
5976 // a builtin: that is how a NESTED composite field records
5977 // which composite it holds.
5978 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5979 self.parse_type_with_implied_flags()?;
5980 fields.push((field_name, field_type));
5981 field_user_types.push(field_user_ref);
5982 if matches!(self.peek(), Token::Comma) {
5983 self.advance();
5984 continue;
5985 }
5986 if matches!(self.peek(), Token::RParen) {
5987 self.advance();
5988 break;
5989 }
5990 return Err(self.err(alloc::format!(
5991 "expected , or ) in composite field list, got {:?}",
5992 self.peek()
5993 )));
5994 }
5995 if fields.is_empty() {
5996 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5997 }
5998 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5999 name,
6000 kind: crate::ast::TypeKind::Composite {
6001 fields,
6002 field_user_types,
6003 },
6004 }));
6005 }
6006 // Required `ENUM` ident.
6007 let kind_ident = match self.peek().clone() {
6008 Token::Ident(s) | Token::QuotedIdent(s) => s,
6009 other => {
6010 return Err(self.err(alloc::format!(
6011 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
6012 )));
6013 }
6014 };
6015 if !kind_ident.eq_ignore_ascii_case("enum") {
6016 return Err(self.err(alloc::format!(
6017 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
6018 )));
6019 }
6020 self.advance();
6021 if !matches!(self.peek(), Token::LParen) {
6022 return Err(self.err(alloc::format!(
6023 "expected '(' after ENUM, got {:?}",
6024 self.peek()
6025 )));
6026 }
6027 self.advance();
6028 let mut labels: Vec<String> = Vec::new();
6029 loop {
6030 match self.peek().clone() {
6031 Token::String(s) => {
6032 self.advance();
6033 labels.push(s);
6034 }
6035 other => {
6036 return Err(
6037 self.err(alloc::format!("expected enum label string, got {other:?}"))
6038 );
6039 }
6040 }
6041 if matches!(self.peek(), Token::Comma) {
6042 self.advance();
6043 continue;
6044 }
6045 if matches!(self.peek(), Token::RParen) {
6046 self.advance();
6047 break;
6048 }
6049 return Err(self.err(alloc::format!(
6050 "expected , or ) in ENUM label list, got {:?}",
6051 self.peek()
6052 )));
6053 }
6054 if labels.is_empty() {
6055 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6056 }
6057 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6058 name,
6059 kind: crate::ast::TypeKind::Enum { labels },
6060 }))
6061 }
6062
6063 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6064 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6065 /// The `CREATE MATERIALIZED VIEW` keywords have already been
6066 /// consumed.
6067 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6068 let if_not_exists = self.parse_if_not_exists();
6069 let name = self.expect_ident_like()?;
6070 let mut columns: Vec<String> = Vec::new();
6071 if matches!(self.peek(), Token::LParen) {
6072 self.advance();
6073 loop {
6074 let c = self.expect_ident_like()?;
6075 columns.push(c);
6076 if matches!(self.peek(), Token::Comma) {
6077 self.advance();
6078 continue;
6079 }
6080 if matches!(self.peek(), Token::RParen) {
6081 self.advance();
6082 break;
6083 }
6084 return Err(self.err(alloc::format!(
6085 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6086 self.peek()
6087 )));
6088 }
6089 }
6090 if !matches!(self.peek(), Token::As) {
6091 return Err(self.err(alloc::format!(
6092 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6093 self.peek()
6094 )));
6095 }
6096 self.advance();
6097 // v7.39 (round 151) — a WITH-headed body is legal (read-only
6098 // CTEs only; the engine rejects data-modifying ones with PG's
6099 // message). A trailing `WITH [NO] DATA` can't START the body,
6100 // so WITH here heads the query.
6101 let body = if self.peek_is_with_kw() {
6102 self.advance();
6103 self.parse_nested_with_select()?
6104 } else {
6105 let body_stmt = self.parse_select_stmt()?;
6106 let Statement::Select(body) = body_stmt else {
6107 return Err(self.err(alloc::format!(
6108 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6109 )));
6110 };
6111 body
6112 };
6113 // Optional trailing `WITH [NO] DATA`.
6114 let with_data = self.parse_optional_with_data(true)?;
6115 Ok(Statement::CreateMaterializedView(
6116 crate::ast::CreateMaterializedViewStatement {
6117 temporary: false,
6118 name,
6119 if_not_exists,
6120 columns,
6121 body,
6122 with_data,
6123 as_plain_table: false,
6124 },
6125 ))
6126 }
6127
6128 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6129 /// `default_when_absent` is what to return if the tail is
6130 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6131 /// WITH DATA).
6132 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6133 let save = self.pos;
6134 // `WITH` is an Ident (not reserved in the lexer).
6135 let is_with = match self.peek() {
6136 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6137 _ => false,
6138 };
6139 if !is_with {
6140 return Ok(default_when_absent);
6141 }
6142 self.advance();
6143 // Optional `NO`.
6144 let mut with_data = true;
6145 let is_no = match self.peek() {
6146 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6147 _ => false,
6148 };
6149 if is_no {
6150 self.advance();
6151 with_data = false;
6152 }
6153 // Required `DATA` ident.
6154 let is_data = match self.peek() {
6155 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6156 _ => false,
6157 };
6158 if is_data {
6159 self.advance();
6160 Ok(with_data)
6161 } else {
6162 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6163 // parser can interpret it.
6164 self.pos = save;
6165 Ok(default_when_absent)
6166 }
6167 }
6168
6169 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6170 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6171 /// All keyword prefixes have already been consumed; the flags
6172 /// say which were present.
6173 fn parse_create_view_after_keyword(
6174 &mut self,
6175 or_replace: bool,
6176 _materialized_unused: bool,
6177 temporary: bool,
6178 ) -> Result<Statement, ParseError> {
6179 let if_not_exists = self.parse_if_not_exists();
6180 let name = self.expect_ident_like()?;
6181 // Optional `(col, col, …)` rename list.
6182 let mut columns: Vec<String> = Vec::new();
6183 if matches!(self.peek(), Token::LParen) {
6184 self.advance();
6185 loop {
6186 let c = self.expect_ident_like()?;
6187 columns.push(c);
6188 if matches!(self.peek(), Token::Comma) {
6189 self.advance();
6190 continue;
6191 }
6192 if matches!(self.peek(), Token::RParen) {
6193 self.advance();
6194 break;
6195 }
6196 return Err(self.err(alloc::format!(
6197 "expected , or ) in VIEW column list, got {:?}",
6198 self.peek()
6199 )));
6200 }
6201 }
6202 // Required `AS`.
6203 if !matches!(self.peek(), Token::As) {
6204 return Err(self.err(alloc::format!(
6205 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6206 self.peek()
6207 )));
6208 }
6209 self.advance();
6210 // Body: a regular SELECT statement. v7.39 (round 151) — a
6211 // WITH-headed body is legal too (read-only CTEs only; the
6212 // engine rejects data-modifying ones with PG's message).
6213 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6214 // with the check-option clause, so WITH here heads the query.
6215 let body = if self.peek_is_with_kw() {
6216 self.advance();
6217 self.parse_nested_with_select()?
6218 } else {
6219 let body_stmt = self.parse_select_stmt()?;
6220 let Statement::Select(body) = body_stmt else {
6221 return Err(self.err(alloc::format!(
6222 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6223 )));
6224 };
6225 body
6226 };
6227 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6228 // The SELECT parser stops before a trailing WITH, so it lands here.
6229 let check_option = if matches!(self.peek(),
6230 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6231 {
6232 self.advance(); // WITH
6233 let opt = match self.peek() {
6234 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6235 self.advance();
6236 crate::ast::ViewCheckOption::Local
6237 }
6238 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6239 self.advance();
6240 crate::ast::ViewCheckOption::Cascaded
6241 }
6242 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6243 _ => crate::ast::ViewCheckOption::Cascaded,
6244 };
6245 if !matches!(self.peek(),
6246 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6247 {
6248 return Err(self.err(alloc::format!(
6249 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6250 self.peek()
6251 )));
6252 }
6253 self.advance(); // CHECK
6254 if !matches!(self.peek(),
6255 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6256 {
6257 return Err(self.err(alloc::format!(
6258 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6259 self.peek()
6260 )));
6261 }
6262 self.advance(); // OPTION
6263 Some(opt)
6264 } else {
6265 None
6266 };
6267 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6268 name,
6269 or_replace,
6270 if_not_exists,
6271 temporary,
6272 columns,
6273 body,
6274 check_option,
6275 }))
6276 }
6277
6278 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6279 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6280 /// consumed; `temporary` carries whether TEMPORARY was seen.
6281 fn parse_create_sequence_after_keyword(
6282 &mut self,
6283 temporary: bool,
6284 ) -> Result<Statement, ParseError> {
6285 let if_not_exists = self.parse_if_not_exists();
6286 let name = self.expect_ident_like()?;
6287 // Optional `AS data_type`.
6288 let data_type = if matches!(self.peek(), Token::As) {
6289 self.advance();
6290 Some(self.parse_sequence_data_type()?)
6291 } else {
6292 None
6293 };
6294 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6295 Ok(Statement::CreateSequence(
6296 crate::ast::CreateSequenceStatement {
6297 name,
6298 if_not_exists,
6299 temporary,
6300 data_type,
6301 options,
6302 },
6303 ))
6304 }
6305
6306 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6307 /// already been consumed; this is reached after `SEQUENCE`.
6308 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6309 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6310 use crate::ast::AlterDomainAction as A;
6311 let name = self.expect_ident_like()?;
6312 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6313 let kw = match self.peek() {
6314 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6315 Token::Drop => alloc::string::String::from("drop"),
6316 Token::Default => alloc::string::String::from("default"),
6317 other => {
6318 return Err(self.err(alloc::format!(
6319 "expected an ALTER DOMAIN action, got {other:?}"
6320 )));
6321 }
6322 };
6323 let action = match kw.as_str() {
6324 "add" => {
6325 self.advance();
6326 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6327 {
6328 self.advance();
6329 Some(self.expect_ident_like()?)
6330 } else {
6331 None
6332 };
6333 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6334 return Err(self.err(alloc::format!(
6335 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6336 self.peek()
6337 )));
6338 }
6339 self.advance();
6340 if !matches!(self.peek(), Token::LParen) {
6341 return Err(self.err("expected '(' after CHECK".into()));
6342 }
6343 self.advance();
6344 let check = self.parse_expr(0)?;
6345 if !matches!(self.peek(), Token::RParen) {
6346 return Err(self.err("expected ')' after CHECK expression".into()));
6347 }
6348 self.advance();
6349 A::AddConstraint { name: cname, check }
6350 }
6351 "drop" => {
6352 self.advance();
6353 match self.peek() {
6354 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6355 self.advance();
6356 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6357 {
6358 self.advance();
6359 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6360 {
6361 return Err(self.err("expected EXISTS after IF".into()));
6362 }
6363 self.advance();
6364 true
6365 } else {
6366 false
6367 };
6368 let cn = self.expect_ident_like()?;
6369 A::DropConstraint {
6370 name: cn,
6371 if_exists,
6372 }
6373 }
6374 Token::Default => {
6375 self.advance();
6376 A::DropDefault
6377 }
6378 Token::Not => {
6379 self.advance();
6380 if !matches!(self.peek(), Token::Null) {
6381 return Err(self.err("expected NULL after NOT".into()));
6382 }
6383 self.advance();
6384 A::DropNotNull
6385 }
6386 other => {
6387 return Err(self.err(alloc::format!(
6388 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6389 )));
6390 }
6391 }
6392 }
6393 "set" => {
6394 self.advance();
6395 match self.peek() {
6396 Token::Default => {
6397 self.advance();
6398 A::SetDefault(self.parse_expr(0)?)
6399 }
6400 Token::Not => {
6401 self.advance();
6402 if !matches!(self.peek(), Token::Null) {
6403 return Err(self.err("expected NULL after NOT".into()));
6404 }
6405 self.advance();
6406 A::SetNotNull
6407 }
6408 other => {
6409 return Err(self.err(alloc::format!(
6410 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6411 )));
6412 }
6413 }
6414 }
6415 "rename" => {
6416 self.advance();
6417 if !matches!(self.peek(), Token::To) {
6418 return Err(self.err("expected TO after RENAME".into()));
6419 }
6420 self.advance();
6421 A::RenameTo(self.expect_ident_like()?)
6422 }
6423 other => {
6424 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6425 }
6426 };
6427 Ok(Statement::AlterDomain { name, action })
6428 }
6429
6430 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6431 let if_exists = self.parse_if_exists();
6432 let name = self.expect_ident_like()?;
6433 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6434 // the option list (PG allows only one or the other).
6435 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6436 self.advance();
6437 if matches!(self.peek(), Token::To) {
6438 self.advance();
6439 } else {
6440 self.expect_keyword_ident("to")?;
6441 }
6442 let new = self.expect_ident_like()?;
6443 return Ok(Statement::AlterSequence(
6444 crate::ast::AlterSequenceStatement {
6445 name,
6446 if_exists,
6447 options: crate::ast::SequenceOptions::default(),
6448 rename_to: Some(new),
6449 },
6450 ));
6451 }
6452 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6453 Ok(Statement::AlterSequence(
6454 crate::ast::AlterSequenceStatement {
6455 name,
6456 if_exists,
6457 options,
6458 rename_to: None,
6459 },
6460 ))
6461 }
6462
6463 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6464 let kw = self.expect_ident_like()?;
6465 match kw.to_ascii_lowercase().as_str() {
6466 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6467 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6468 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6469 other => Err(self.err(alloc::format!(
6470 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6471 ))),
6472 }
6473 }
6474
6475 fn parse_sequence_options(
6476 &mut self,
6477 allow_restart: bool,
6478 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6479 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6480 let mut opts = SequenceOptions::default();
6481 #[allow(clippy::while_let_loop)]
6482 loop {
6483 // Match an ident; stop at any non-ident token (sentinel,
6484 // semicolon, end of statement).
6485 let kw_lc = match self.peek() {
6486 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6487 _ => break,
6488 };
6489 match kw_lc.as_str() {
6490 "increment" => {
6491 self.advance();
6492 // Optional BY.
6493 if self.peek_is_by() {
6494 self.advance();
6495 }
6496 opts.increment = Some(self.expect_signed_int()?);
6497 }
6498 "minvalue" => {
6499 self.advance();
6500 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6501 }
6502 "maxvalue" => {
6503 self.advance();
6504 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6505 }
6506 "no" => {
6507 self.advance();
6508 let what = self.expect_ident_like()?;
6509 match what.to_ascii_lowercase().as_str() {
6510 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6511 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6512 "cycle" => opts.cycle = Some(false),
6513 other => {
6514 return Err(self.err(alloc::format!(
6515 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6516 )));
6517 }
6518 }
6519 }
6520 "start" => {
6521 self.advance();
6522 // Optional WITH.
6523 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6524 if s.eq_ignore_ascii_case("with"))
6525 {
6526 self.advance();
6527 }
6528 opts.start = Some(self.expect_signed_int()?);
6529 }
6530 "restart" if allow_restart => {
6531 self.advance();
6532 // Optional WITH n; bare RESTART means restart at START.
6533 let mut with_val: Option<i64> = None;
6534 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6535 if s.eq_ignore_ascii_case("with"))
6536 {
6537 self.advance();
6538 with_val = Some(self.expect_signed_int()?);
6539 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6540 with_val = Some(self.expect_signed_int()?);
6541 }
6542 opts.restart = Some(with_val);
6543 }
6544 "cache" => {
6545 self.advance();
6546 opts.cache = Some(self.expect_signed_int()?);
6547 }
6548 "cycle" => {
6549 self.advance();
6550 opts.cycle = Some(true);
6551 }
6552 "owned" => {
6553 self.advance();
6554 match self.peek() {
6555 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6556 self.advance();
6557 }
6558 other => {
6559 return Err(
6560 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6561 );
6562 }
6563 }
6564 // OWNED BY {NONE | tab.col}. Read just one ident
6565 // (NOT expect_ident_like which would auto-strip
6566 // a schema prefix and consume the `.col` we need).
6567 let first = match self.advance() {
6568 Token::Ident(s) | Token::QuotedIdent(s) => s,
6569 other => {
6570 return Err(self.err(alloc::format!(
6571 "expected identifier or NONE after OWNED BY, got {other:?}"
6572 )));
6573 }
6574 };
6575 if first.eq_ignore_ascii_case("none") {
6576 opts.owned_by = Some(SequenceOwnedBy::None);
6577 } else if matches!(self.peek(), Token::Dot) {
6578 self.advance();
6579 let second = match self.advance() {
6580 Token::Ident(s) | Token::QuotedIdent(s) => s,
6581 other => {
6582 return Err(self.err(alloc::format!(
6583 "expected column name after OWNED BY {first}., got {other:?}"
6584 )));
6585 }
6586 };
6587 // v7.17 dump-compat fix — pg_dump emits
6588 // OWNED BY clauses as
6589 // `schema.table.column` (three segments).
6590 // If a third `.<ident>` follows, treat the
6591 // first ident as schema (drop it; SPG is
6592 // single-schema) and the middle / last
6593 // pair as table.column. Otherwise it's
6594 // the two-segment form table.column.
6595 if matches!(self.peek(), Token::Dot) {
6596 self.advance();
6597 let third = match self.advance() {
6598 Token::Ident(s) | Token::QuotedIdent(s) => s,
6599 other => {
6600 return Err(self.err(alloc::format!(
6601 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6602 )));
6603 }
6604 };
6605 let _ = first; // schema prefix discarded
6606 opts.owned_by = Some(SequenceOwnedBy::Column {
6607 table: second,
6608 column: third,
6609 });
6610 } else {
6611 opts.owned_by = Some(SequenceOwnedBy::Column {
6612 table: first,
6613 column: second,
6614 });
6615 }
6616 } else {
6617 return Err(self.err(alloc::format!(
6618 "expected table.column or NONE after OWNED BY, got {first:?}"
6619 )));
6620 }
6621 }
6622 _ => break,
6623 }
6624 }
6625 Ok(opts)
6626 }
6627
6628 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6629 let neg = if matches!(self.peek(), Token::Minus) {
6630 self.advance();
6631 true
6632 } else {
6633 false
6634 };
6635 match self.peek() {
6636 Token::Integer(n) => {
6637 let v = *n;
6638 self.advance();
6639 Ok(if neg { -v } else { v })
6640 }
6641 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6642 }
6643 }
6644
6645 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6646 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6647 /// clause is fully accepted and discarded — SPG always runs
6648 /// constraint checks immediately (single-writer model). The
6649 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6650 /// in either order (per the SQL spec they're independent),
6651 /// though pg_dump always emits them in the canonical
6652 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6653 /// Stops at the first token that isn't part of the clause.
6654 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6655 self.consume_deferrable_clauses_timed().map(|_| ())
6656 }
6657
6658 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6659 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6660 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6661 /// NOT DEFERRABLE and a circular-FK migration could not load.
6662 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6663 let mut deferrable = false;
6664 let mut initially_deferred = false;
6665 loop {
6666 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6667 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6668 self.advance();
6669 deferrable = true;
6670 if self.consume_optional_initially_clause()? {
6671 initially_deferred = true;
6672 }
6673 continue;
6674 }
6675 // `NOT DEFERRABLE` — already worked pre-3.1.
6676 if matches!(self.peek(), Token::Not) {
6677 let look = self.tokens.get(self.pos + 1);
6678 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6679 self.advance(); // NOT
6680 self.advance(); // DEFERRABLE
6681 deferrable = false;
6682 initially_deferred = false;
6683 let _ = self.consume_optional_initially_clause()?;
6684 continue;
6685 }
6686 break;
6687 }
6688 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6689 // accepts this without a leading [NOT] DEFERRABLE
6690 // (the timing keyword alone). pg_dump occasionally
6691 // emits it on FK constraints that inherit timing.
6692 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6693 if self.consume_optional_initially_clause()? {
6694 initially_deferred = true;
6695 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6696 deferrable = true;
6697 }
6698 continue;
6699 }
6700 break;
6701 }
6702 Ok((deferrable, initially_deferred))
6703 }
6704
6705 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6706 /// next token is `INITIALLY`, consume it plus the required
6707 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6708 /// Returns true when the timing seen was `DEFERRED`.
6709 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6710 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6711 return Ok(false);
6712 }
6713 self.advance(); // INITIALLY
6714 match self.advance() {
6715 Token::Ident(s)
6716 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6717 {
6718 Ok(s.eq_ignore_ascii_case("deferred"))
6719 }
6720 other => Err(self.err(alloc::format!(
6721 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6722 ))),
6723 }
6724 }
6725
6726 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6727 /// in its entirety so the parser returns Empty without
6728 /// touching the runtime. The CREATE+PROCEDURE keywords are
6729 /// already consumed; this swallows everything from the
6730 /// procedure name through the matching `END`, including
6731 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6732 /// (DELIMITER `//` makes the script splitter forward the
6733 /// whole block as one statement), `@var` session-variable
6734 /// references, and the trailing terminator.
6735 ///
6736 /// Tracks nesting depth so:
6737 /// BEGIN
6738 /// IF cond THEN
6739 /// BEGIN ... END;
6740 /// END IF;
6741 /// END
6742 /// terminates at the outer END.
6743 fn consume_mysql_routine_body(&mut self) {
6744 // Outer skeleton: name, (...), optional clauses, BEGIN
6745 // <body> END [;]. Scan for the first BEGIN — anything
6746 // before it is signature decoration we don't care about.
6747 // Once inside BEGIN, count up on BEGIN, down on END.
6748 let mut depth: i32 = 0;
6749 let mut started = false;
6750 loop {
6751 match self.peek().clone() {
6752 Token::Begin => {
6753 self.advance();
6754 depth += 1;
6755 started = true;
6756 }
6757 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6758 self.advance();
6759 if started {
6760 depth -= 1;
6761 if depth <= 0 {
6762 // Optional trailing ident (`END IF`,
6763 // `END LOOP`, `END WHILE`, `END CASE`,
6764 // `END label_name`) — eat the next
6765 // ident if present so we don't
6766 // mistake `END IF;` for the outer
6767 // close.
6768 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6769 // If the next token is one of the
6770 // PL/SQL block-closer keywords,
6771 // the END belongs to an inner
6772 // block; bump depth back up.
6773 let is_inner_close = matches!(
6774 self.peek(),
6775 Token::Ident(s) | Token::QuotedIdent(s)
6776 if matches!(
6777 s.to_ascii_lowercase().as_str(),
6778 "if" | "loop" | "while" | "case" | "repeat"
6779 )
6780 );
6781 if is_inner_close {
6782 self.advance();
6783 depth += 1;
6784 continue;
6785 }
6786 }
6787 // Eat optional trailing `;`.
6788 if matches!(self.peek(), Token::Semicolon) {
6789 self.advance();
6790 }
6791 return;
6792 }
6793 }
6794 }
6795 Token::Eof => return,
6796 _ => {
6797 self.advance();
6798 }
6799 }
6800 }
6801 }
6802
6803 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6804 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6805 ///
6806 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6807 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6808 /// ident, or `ident @ ident-or-quoted-string` host form)
6809 /// * `SQL SECURITY {DEFINER|INVOKER}`
6810 ///
6811 /// Each clause may appear at most once but in any order.
6812 /// The hints are pure planner / permission metadata that
6813 /// SPG's view-rewrite engine handles uniformly; we accept
6814 /// and discard. Returns `Ok(())` once a non-clause token is
6815 /// peeked (the caller then checks for the `VIEW` keyword).
6816 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6817 loop {
6818 match self.peek().clone() {
6819 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6820 self.advance(); // ALGORITHM
6821 // Optional `=`. MySQL spec requires it but be
6822 // generous.
6823 if matches!(self.peek(), Token::Eq) {
6824 self.advance();
6825 }
6826 // UNDEFINED / MERGE / TEMPTABLE — accept any
6827 // bare ident; unknown values still parse so
6828 // future MySQL versions don't break.
6829 if matches!(
6830 self.peek(),
6831 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6832 ) {
6833 self.advance();
6834 }
6835 }
6836 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6837 self.advance(); // DEFINER
6838 if matches!(self.peek(), Token::Eq) {
6839 self.advance();
6840 }
6841 // User: quoted string, ident, OR ident @ host
6842 // (host may itself be quoted or bare).
6843 match self.peek().clone() {
6844 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6845 self.advance();
6846 // Optional `@host`.
6847 if matches!(self.peek(), Token::At) {
6848 self.advance();
6849 if matches!(
6850 self.peek(),
6851 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6852 ) {
6853 self.advance();
6854 }
6855 }
6856 }
6857 _ => {}
6858 }
6859 }
6860 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6861 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6862 // when followed by SECURITY — the dispatcher must
6863 // not consume a bare `SQL` token (it's not a
6864 // legal CREATE prefix on its own).
6865 let save = self.pos;
6866 self.advance(); // SQL
6867 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6868 if s2.eq_ignore_ascii_case("security"))
6869 {
6870 self.advance(); // SECURITY
6871 // DEFINER / INVOKER trailing ident.
6872 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6873 self.advance();
6874 }
6875 } else {
6876 // Not a SQL SECURITY clause — roll back and
6877 // bail; the caller will error out cleanly.
6878 self.pos = save;
6879 return Ok(());
6880 }
6881 }
6882 _ => return Ok(()),
6883 }
6884 }
6885 }
6886
6887 fn parse_if_not_exists(&mut self) -> bool {
6888 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6889 {
6890 let save = self.pos;
6891 self.advance();
6892 if matches!(self.peek(), Token::Not) {
6893 self.advance();
6894 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6895 {
6896 self.advance();
6897 return true;
6898 }
6899 }
6900 self.pos = save;
6901 }
6902 false
6903 }
6904
6905 fn parse_if_exists(&mut self) -> bool {
6906 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6907 {
6908 let save = self.pos;
6909 self.advance();
6910 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6911 {
6912 self.advance();
6913 return true;
6914 }
6915 self.pos = save;
6916 }
6917 false
6918 }
6919
6920 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6921 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6922 /// been consumed.
6923 fn parse_create_trigger_after_keyword(
6924 &mut self,
6925 or_replace: bool,
6926 ) -> Result<Statement, ParseError> {
6927 let name = self.expect_ident_like()?;
6928 let timing = {
6929 let ident = self.expect_ident_like()?;
6930 if ident.eq_ignore_ascii_case("before") {
6931 TriggerTiming::Before
6932 } else if ident.eq_ignore_ascii_case("after") {
6933 TriggerTiming::After
6934 } else if ident.eq_ignore_ascii_case("instead") {
6935 let next = self.expect_ident_like()?;
6936 if !next.eq_ignore_ascii_case("of") {
6937 return Err(self.err(alloc::format!(
6938 "expected OF after INSTEAD in trigger timing, got {next:?}"
6939 )));
6940 }
6941 TriggerTiming::InsteadOf
6942 } else {
6943 return Err(self.err(alloc::format!(
6944 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6945 )));
6946 }
6947 };
6948 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6949 // OR is a reserved keyword token (Token::Or), not an Ident.
6950 // v7.13.0 — after an UPDATE event we may optionally see
6951 // `OF col, col, …` (mailrs round-5 G7). Columns are
6952 // captured into `update_columns` once across the whole
6953 // events list; multiple `UPDATE OF` clauses are rejected.
6954 let mut events: Vec<TriggerEvent> = Vec::new();
6955 let mut update_columns: Vec<String> = Vec::new();
6956 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6957 events.push(first_ev);
6958 if !first_cols.is_empty() {
6959 update_columns = first_cols;
6960 }
6961 while matches!(self.peek(), Token::Or) {
6962 self.advance();
6963 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6964 events.push(ev);
6965 if !cols.is_empty() {
6966 if !update_columns.is_empty() {
6967 return Err(
6968 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6969 );
6970 }
6971 update_columns = cols;
6972 }
6973 }
6974 // ON <table>
6975 let tok = self.peek();
6976 let Token::On = tok else {
6977 return Err(self.err(alloc::format!(
6978 "expected ON after trigger events, got {tok:?}"
6979 )));
6980 };
6981 self.advance();
6982 let table = self.expect_ident_like()?;
6983 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6984 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6985 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6986 // the trigger as a plain AFTER trigger (correct for every non-deferred
6987 // use; deferral timing is not yet honoured).
6988 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6989 if s.eq_ignore_ascii_case("from"))
6990 {
6991 self.advance();
6992 let _reftable = self.expect_ident_like()?;
6993 }
6994 self.consume_optional_deferrable_clauses()?;
6995 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6996 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6997 // idents.
6998 if !matches!(self.peek(), Token::For) {
6999 return Err(self.err(alloc::format!(
7000 "expected FOR EACH ROW / STATEMENT, got {:?}",
7001 self.peek()
7002 )));
7003 }
7004 self.advance();
7005 let for_each = {
7006 let e = self.expect_ident_like()?;
7007 if !e.eq_ignore_ascii_case("each") {
7008 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
7009 }
7010 let unit = self.expect_ident_like()?;
7011 if unit.eq_ignore_ascii_case("row") {
7012 TriggerForEach::Row
7013 } else if unit.eq_ignore_ascii_case("statement") {
7014 TriggerForEach::Statement
7015 } else {
7016 return Err(self.err(alloc::format!(
7017 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
7018 )));
7019 }
7020 };
7021 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
7022 let when_condition = if matches!(self.peek(),
7023 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7024 {
7025 self.advance();
7026 Some(self.parse_paren_expr("WHEN")?)
7027 } else {
7028 None
7029 };
7030 // EXECUTE FUNCTION/PROCEDURE name(...)
7031 let exec = self.expect_ident_like()?;
7032 if !exec.eq_ignore_ascii_case("execute") {
7033 return Err(self.err(alloc::format!(
7034 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7035 )));
7036 }
7037 let fn_or_proc = self.expect_ident_like()?;
7038 if !(fn_or_proc.eq_ignore_ascii_case("function")
7039 || fn_or_proc.eq_ignore_ascii_case("procedure"))
7040 {
7041 return Err(self.err(alloc::format!(
7042 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7043 )));
7044 }
7045 let function = self.expect_ident_like()?;
7046 // Optional empty arg list `()`.
7047 if matches!(self.peek(), Token::LParen) {
7048 self.advance();
7049 if !matches!(self.peek(), Token::RParen) {
7050 return Err(self.err(alloc::format!(
7051 "v7.12.4 trigger function calls take no args; got {:?}",
7052 self.peek()
7053 )));
7054 }
7055 self.advance();
7056 }
7057 Ok(Statement::CreateTrigger(CreateTriggerStatement {
7058 name,
7059 or_replace,
7060 timing,
7061 events,
7062 table,
7063 for_each,
7064 function,
7065 update_columns,
7066 when_condition,
7067 }))
7068 }
7069
7070 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7071 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7072 fn parse_create_rule_after_keyword(
7073 &mut self,
7074 or_replace: bool,
7075 ) -> Result<Statement, ParseError> {
7076 let name = self.expect_ident_like()?;
7077 if !matches!(self.peek(), Token::As) {
7078 return Err(self.err(alloc::format!(
7079 "expected AS in CREATE RULE, got {:?}",
7080 self.peek()
7081 )));
7082 }
7083 self.advance();
7084 if !matches!(self.peek(), Token::On) {
7085 return Err(self.err(alloc::format!(
7086 "expected ON in CREATE RULE, got {:?}",
7087 self.peek()
7088 )));
7089 }
7090 self.advance();
7091 let event = self.parse_rule_event()?;
7092 if !matches!(self.peek(), Token::To)
7093 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7094 {
7095 return Err(self.err(alloc::format!(
7096 "expected TO after rule event, got {:?}",
7097 self.peek()
7098 )));
7099 }
7100 self.advance();
7101 let table = self.expect_ident_like()?;
7102 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7103 let when_condition = if matches!(self.peek(), Token::Where) {
7104 self.advance();
7105 Some(self.parse_expr(0)?)
7106 } else {
7107 None
7108 };
7109 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7110 {
7111 return Err(self.err(alloc::format!(
7112 "expected DO in CREATE RULE, got {:?}",
7113 self.peek()
7114 )));
7115 }
7116 self.advance();
7117 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7118 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7119 {
7120 self.advance();
7121 true
7122 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7123 self.advance();
7124 false
7125 } else {
7126 false
7127 };
7128 // `NOTHING` | `( cmd; … )` | `cmd`.
7129 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7130 {
7131 self.advance();
7132 Vec::new()
7133 } else if matches!(self.peek(), Token::LParen) {
7134 self.advance();
7135 let mut cmds = Vec::new();
7136 loop {
7137 cmds.push(self.parse_one_statement()?);
7138 if matches!(self.peek(), Token::Semicolon) {
7139 self.advance();
7140 if matches!(self.peek(), Token::RParen) {
7141 break;
7142 }
7143 continue;
7144 }
7145 break;
7146 }
7147 if !matches!(self.peek(), Token::RParen) {
7148 return Err(self.err(alloc::format!(
7149 "expected ) closing the CREATE RULE command list, got {:?}",
7150 self.peek()
7151 )));
7152 }
7153 self.advance();
7154 cmds
7155 } else {
7156 alloc::vec![self.parse_one_statement()?]
7157 };
7158 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7159 name,
7160 or_replace,
7161 event,
7162 table,
7163 instead,
7164 when_condition,
7165 commands,
7166 }))
7167 }
7168
7169 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7170 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7171 if matches!(self.peek(), Token::Insert) {
7172 self.advance();
7173 return Ok(alloc::string::String::from("INSERT"));
7174 }
7175 if matches!(self.peek(), Token::Select) {
7176 self.advance();
7177 return Ok(alloc::string::String::from("SELECT"));
7178 }
7179 match self.peek() {
7180 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7181 self.advance();
7182 Ok(alloc::string::String::from("UPDATE"))
7183 }
7184 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7185 self.advance();
7186 Ok(alloc::string::String::from("DELETE"))
7187 }
7188 other => Err(self.err(alloc::format!(
7189 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7190 ))),
7191 }
7192 }
7193
7194 /// v7.13.0 — parse one trigger event, then optionally consume
7195 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7196 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7197 fn parse_trigger_event_with_optional_of(
7198 &mut self,
7199 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7200 let ev = self.parse_trigger_event()?;
7201 if !matches!(ev, TriggerEvent::Update) {
7202 return Ok((ev, Vec::new()));
7203 }
7204 // `OF` is a bare ident.
7205 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7206 return Ok((ev, Vec::new()));
7207 }
7208 self.advance(); // OF
7209 let mut cols: Vec<String> = Vec::new();
7210 loop {
7211 cols.push(self.expect_ident_like()?);
7212 if matches!(self.peek(), Token::Comma) {
7213 self.advance();
7214 continue;
7215 }
7216 break;
7217 }
7218 if cols.is_empty() {
7219 return Err(
7220 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7221 );
7222 }
7223 Ok((ev, cols))
7224 }
7225
7226 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7227 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7228 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7229 /// inside the body.
7230 /// Called by [`parse_plpgsql_body`] after the body's tokens
7231 /// have been lexed into this temporary parser.
7232 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7233 // v7.12.6 — optional DECLARE prelude.
7234 let declarations = if matches!(
7235 self.peek(),
7236 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7237 ) {
7238 self.advance();
7239 self.parse_plpgsql_declare_block()?
7240 } else {
7241 Vec::new()
7242 };
7243 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7244 // `BEGIN` transaction-start, but we can reuse the
7245 // reserved Token::Begin since the body is a separate
7246 // lex/parse context).
7247 if !matches!(self.peek(), Token::Begin) {
7248 return Err(self.err(alloc::format!(
7249 "expected BEGIN at start of plpgsql block, got {:?}",
7250 self.peek()
7251 )));
7252 }
7253 self.advance();
7254 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7255 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7256 // body's last statement and the trailing END. When present
7257 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7258 // arms terminated by END.
7259 let exception_handlers = if matches!(
7260 self.peek(),
7261 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7262 ) {
7263 self.advance();
7264 self.parse_plpgsql_exception_handlers()?
7265 } else {
7266 Vec::new()
7267 };
7268 Ok(PlPgSqlBlock {
7269 declarations,
7270 statements,
7271 exception_handlers,
7272 })
7273 }
7274
7275 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7276 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7277 fn parse_plpgsql_exception_handlers(
7278 &mut self,
7279 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7280 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7281 loop {
7282 // Stop at END — the block-level trailing END LOOP / END;
7283 // is handled by the caller.
7284 if matches!(
7285 self.peek(),
7286 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7287 ) {
7288 return Ok(out);
7289 }
7290 // WHEN <cond> [OR <cond>]* THEN <body>
7291 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7292 {
7293 return Err(self.err(alloc::format!(
7294 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7295 self.peek()
7296 )));
7297 }
7298 self.advance();
7299 let mut conditions: Vec<String> = Vec::new();
7300 conditions.push(self.expect_ident_like()?);
7301 while matches!(self.peek(), Token::Or) {
7302 self.advance();
7303 conditions.push(self.expect_ident_like()?);
7304 }
7305 let then_kw = self.expect_ident_like()?;
7306 if !then_kw.eq_ignore_ascii_case("then") {
7307 return Err(self.err(alloc::format!(
7308 "expected THEN after WHEN condition list, got {then_kw:?}"
7309 )));
7310 }
7311 let body = self.parse_plpgsql_stmt_list_until_end()?;
7312 out.push(crate::ast::ExceptionHandler { conditions, body });
7313 }
7314 }
7315
7316 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7317 /// prelude. Caller has already consumed `DECLARE`. We stop
7318 /// reading entries when we hit `BEGIN`.
7319 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7320 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7321 loop {
7322 if matches!(self.peek(), Token::Begin) {
7323 return Ok(out);
7324 }
7325 let name = self.expect_ident_like()?;
7326 // v7.37.20 (20.7) — type inference: if the next token is
7327 // `:=` or `=` (no explicit type), infer from the default
7328 // expression. Otherwise the ident that follows is the
7329 // declared type.
7330 //
7331 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7332 // (PG-standard). SPG parse-accepts and treats identically
7333 // to inference — the eventual runtime value determines
7334 // the local's type, which is faithful to how SPG handles
7335 // untyped locals today (see 20.7). Full compile-time
7336 // catalog lookup queues with v7.40 PL/pgSQL epic.
7337 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7338 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7339 // downstream declaration walker to type the local by
7340 // the runtime type of the default expression.
7341 FunctionArgType::Raw("_infer_".into())
7342 } else {
7343 let ty_token = self.expect_ident_like()?;
7344 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7345 // consume optional `.<ident>` qualifier + `%<KW>`
7346 // suffix. Both qualifier and suffix map to _infer_.
7347 if matches!(self.peek(), Token::Dot) {
7348 self.advance();
7349 let _ = self.expect_ident_like()?;
7350 }
7351 if matches!(self.peek(), Token::Percent) {
7352 self.advance();
7353 // Consume the trailing TYPE / ROWTYPE ident.
7354 let _ = self.expect_ident_like()?;
7355 FunctionArgType::Raw("_infer_".into())
7356 } else {
7357 match map_type_ident_to_column_type_name(&ty_token) {
7358 Some(t) => FunctionArgType::Typed(t),
7359 None => FunctionArgType::Raw(ty_token),
7360 }
7361 }
7362 };
7363 let default = match self.peek() {
7364 Token::ColonEq => {
7365 self.advance();
7366 Some(self.parse_expr(0)?)
7367 }
7368 Token::Eq => {
7369 // PL/pgSQL also accepts `=` for the
7370 // DECLARE default (PG treats them the same
7371 // in this position).
7372 self.advance();
7373 Some(self.parse_expr(0)?)
7374 }
7375 _ => None,
7376 };
7377 // Mandatory `;` between declarations.
7378 if !matches!(self.peek(), Token::Semicolon) {
7379 return Err(self.err(alloc::format!(
7380 "expected ; after DECLARE entry for {name:?}, got {:?}",
7381 self.peek()
7382 )));
7383 }
7384 self.advance();
7385 out.push(PlPgSqlDeclare { name, ty, default });
7386 }
7387 }
7388
7389 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7390 /// the terminating `END;` (or `END IF;` etc — handled by the
7391 /// per-construct sub-parsers). Used by both the outer block
7392 /// and the IF/ELSE branch bodies.
7393 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7394 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7395 loop {
7396 // Allow trailing semicolons + END.
7397 while matches!(self.peek(), Token::Semicolon) {
7398 self.advance();
7399 }
7400 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7401 if matches!(
7402 self.peek(),
7403 Token::Ident(s) | Token::QuotedIdent(s)
7404 if s.eq_ignore_ascii_case("end")
7405 || s.eq_ignore_ascii_case("else")
7406 || s.eq_ignore_ascii_case("elsif")
7407 || s.eq_ignore_ascii_case("elseif")
7408 || s.eq_ignore_ascii_case("exception")
7409 || s.eq_ignore_ascii_case("when")
7410 ) {
7411 return Ok(statements);
7412 }
7413 // Otherwise: one statement, then expect `;` or
7414 // a block-terminator keyword.
7415 let stmt = self.parse_plpgsql_stmt()?;
7416 statements.push(stmt);
7417 match self.peek() {
7418 Token::Semicolon => {
7419 self.advance();
7420 }
7421 Token::Ident(s) | Token::QuotedIdent(s)
7422 if s.eq_ignore_ascii_case("end")
7423 || s.eq_ignore_ascii_case("else")
7424 || s.eq_ignore_ascii_case("elsif")
7425 || s.eq_ignore_ascii_case("elseif")
7426 || s.eq_ignore_ascii_case("exception")
7427 || s.eq_ignore_ascii_case("when") =>
7428 {
7429 // Final statement of the block without `;`.
7430 }
7431 other => {
7432 return Err(self.err(alloc::format!(
7433 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7434 )));
7435 }
7436 }
7437 }
7438 }
7439
7440 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7441 // RETURN keyword?
7442 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7443 {
7444 self.advance();
7445 return self.parse_plpgsql_return();
7446 }
7447 // v7.12.6 — IF block.
7448 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7449 {
7450 self.advance();
7451 return self.parse_plpgsql_if();
7452 }
7453 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7454 // Detected by peeking that token pos+3 is Ident("execute").
7455 if matches!(self.peek(), Token::For)
7456 && matches!(
7457 self.tokens.get(self.pos + 1),
7458 Some(Token::Ident(_) | Token::QuotedIdent(_))
7459 )
7460 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7461 && matches!(
7462 self.tokens.get(self.pos + 3),
7463 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7464 )
7465 {
7466 self.advance(); // FOR
7467 let var = self.expect_ident_like()?;
7468 self.advance(); // IN
7469 self.advance(); // EXECUTE
7470 // Prescan for LOOP at paren depth 0 so parse_expr stops
7471 // before the LOOP keyword (same trick as the bare-SELECT
7472 // ForQuery arm).
7473 let mut depth: i32 = 0;
7474 let mut loop_pos: Option<usize> = None;
7475 let mut scan = self.pos;
7476 while scan < self.tokens.len() {
7477 match self.tokens.get(scan) {
7478 Some(Token::LParen) => depth += 1,
7479 Some(Token::RParen) => depth -= 1,
7480 Some(Token::Ident(s) | Token::QuotedIdent(s))
7481 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7482 {
7483 loop_pos = Some(scan);
7484 break;
7485 }
7486 _ => {}
7487 }
7488 scan += 1;
7489 }
7490 let loop_pos = loop_pos.ok_or_else(|| {
7491 self.err(alloc::format!(
7492 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7493 ))
7494 })?;
7495 let saved_loop = self.tokens[loop_pos].clone();
7496 self.tokens[loop_pos] = Token::Semicolon;
7497 let expr_result = self.parse_expr(0);
7498 self.tokens[loop_pos] = saved_loop;
7499 let sql_expr = expr_result?;
7500 let loop_kw = self.expect_ident_like()?;
7501 if !loop_kw.eq_ignore_ascii_case("loop") {
7502 return Err(self.err(alloc::format!(
7503 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7504 )));
7505 }
7506 let body = self.parse_plpgsql_stmt_list_until_end()?;
7507 let end_kw = self.expect_ident_like()?;
7508 if !end_kw.eq_ignore_ascii_case("end") {
7509 return Err(self.err(alloc::format!(
7510 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7511 )));
7512 }
7513 let loop_kw2 = self.expect_ident_like()?;
7514 if !loop_kw2.eq_ignore_ascii_case("loop") {
7515 return Err(self.err(alloc::format!(
7516 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7517 )));
7518 }
7519 return Ok(PlPgSqlStmt::ForExecute {
7520 var,
7521 sql_expr,
7522 body,
7523 });
7524 }
7525 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7526 //
7527 // Two syntactic forms:
7528 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7529 // FOR var IN (SELECT ...) LOOP ...
7530 //
7531 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7532 // the trailing `LOOP` keyword as a table alias, we prescan
7533 // forward to find LOOP at paren depth 0, splice a fake
7534 // Semicolon at that position (so SELECT parses cleanly),
7535 // then re-splice LOOP back in.
7536 //
7537 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7538 // LOOP directly — no scan required.
7539 if matches!(self.peek(), Token::For)
7540 && matches!(
7541 self.tokens.get(self.pos + 1),
7542 Some(Token::Ident(_) | Token::QuotedIdent(_))
7543 )
7544 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7545 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7546 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7547 {
7548 self.advance(); // FOR
7549 let var = self.expect_ident_like()?;
7550 // IN
7551 self.advance();
7552 let query = if matches!(self.peek(), Token::LParen) {
7553 // Paren-wrapped SELECT.
7554 self.advance();
7555 let inner = self.parse_select_stmt()?;
7556 let Statement::Select(q) = inner else {
7557 return Err(self.err(alloc::format!(
7558 "expected SELECT inside (…), got {:?}",
7559 self.peek()
7560 )));
7561 };
7562 if !matches!(self.peek(), Token::RParen) {
7563 return Err(self.err(alloc::format!(
7564 "expected ')' after FOR-IN-SELECT body, got {:?}",
7565 self.peek()
7566 )));
7567 }
7568 self.advance();
7569 q
7570 } else {
7571 // Bare SELECT: prescan to find the LOOP boundary.
7572 let mut depth: i32 = 0;
7573 let mut loop_pos: Option<usize> = None;
7574 let mut scan = self.pos;
7575 while scan < self.tokens.len() {
7576 match self.tokens.get(scan) {
7577 Some(Token::LParen) => depth += 1,
7578 Some(Token::RParen) => depth -= 1,
7579 Some(Token::Ident(s) | Token::QuotedIdent(s))
7580 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7581 {
7582 loop_pos = Some(scan);
7583 break;
7584 }
7585 _ => {}
7586 }
7587 scan += 1;
7588 }
7589 let loop_pos = loop_pos.ok_or_else(|| {
7590 self.err(alloc::format!(
7591 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7592 ))
7593 })?;
7594 // Swap the LOOP token with a synthetic Semicolon so
7595 // parse_select_stmt stops there, then restore afterward.
7596 let saved_loop = self.tokens[loop_pos].clone();
7597 self.tokens[loop_pos] = Token::Semicolon;
7598 let parse_result = self.parse_select_stmt();
7599 self.tokens[loop_pos] = saved_loop;
7600 let inner = parse_result?;
7601 let Statement::Select(q) = inner else {
7602 return Err(self.err(alloc::format!(
7603 "expected SELECT after FOR <var> IN, got {:?}",
7604 self.peek()
7605 )));
7606 };
7607 q
7608 };
7609 let loop_kw = self.expect_ident_like()?;
7610 if !loop_kw.eq_ignore_ascii_case("loop") {
7611 return Err(self.err(alloc::format!(
7612 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7613 )));
7614 }
7615 let body = self.parse_plpgsql_stmt_list_until_end()?;
7616 let end_kw = self.expect_ident_like()?;
7617 if !end_kw.eq_ignore_ascii_case("end") {
7618 return Err(self.err(alloc::format!(
7619 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7620 )));
7621 }
7622 let loop_kw2 = self.expect_ident_like()?;
7623 if !loop_kw2.eq_ignore_ascii_case("loop") {
7624 return Err(self.err(alloc::format!(
7625 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7626 )));
7627 }
7628 return Ok(PlPgSqlStmt::ForQuery {
7629 var,
7630 query: Box::new(query),
7631 body,
7632 });
7633 }
7634 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7635 // FOR is a reserved keyword token (Token::For).
7636 if matches!(self.peek(), Token::For)
7637 && matches!(
7638 self.tokens.get(self.pos + 1),
7639 Some(Token::Ident(_) | Token::QuotedIdent(_))
7640 )
7641 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7642 {
7643 self.advance(); // FOR
7644 let var = self.expect_ident_like()?;
7645 if !matches!(self.peek(), Token::In) {
7646 return Err(self.err(alloc::format!(
7647 "expected IN after FOR <var>, got {:?}",
7648 self.peek()
7649 )));
7650 }
7651 self.advance();
7652 let reverse = matches!(
7653 self.peek(),
7654 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7655 );
7656 if reverse {
7657 self.advance();
7658 }
7659 let start = self.parse_expr(0)?;
7660 if !matches!(self.peek(), Token::DotDot) {
7661 return Err(self.err(alloc::format!(
7662 "expected '..' between FOR loop bounds, got {:?}",
7663 self.peek()
7664 )));
7665 }
7666 self.advance();
7667 let end = self.parse_expr(0)?;
7668 let loop_kw = self.expect_ident_like()?;
7669 if !loop_kw.eq_ignore_ascii_case("loop") {
7670 return Err(self.err(alloc::format!(
7671 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7672 )));
7673 }
7674 let body = self.parse_plpgsql_stmt_list_until_end()?;
7675 let end_kw = self.expect_ident_like()?;
7676 if !end_kw.eq_ignore_ascii_case("end") {
7677 return Err(self.err(alloc::format!(
7678 "expected END LOOP after FOR body, got {end_kw:?}"
7679 )));
7680 }
7681 let loop_kw2 = self.expect_ident_like()?;
7682 if !loop_kw2.eq_ignore_ascii_case("loop") {
7683 return Err(self.err(alloc::format!(
7684 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7685 )));
7686 }
7687 return Ok(PlPgSqlStmt::ForRange {
7688 var,
7689 start,
7690 end,
7691 reverse,
7692 body,
7693 });
7694 }
7695 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7696 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7697 {
7698 self.advance();
7699 let body = self.parse_plpgsql_stmt_list_until_end()?;
7700 let end_kw = self.expect_ident_like()?;
7701 if !end_kw.eq_ignore_ascii_case("end") {
7702 return Err(self.err(alloc::format!(
7703 "expected END LOOP after LOOP body, got {end_kw:?}"
7704 )));
7705 }
7706 let loop_kw = self.expect_ident_like()?;
7707 if !loop_kw.eq_ignore_ascii_case("loop") {
7708 return Err(self.err(alloc::format!(
7709 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7710 )));
7711 }
7712 return Ok(PlPgSqlStmt::Loop { body });
7713 }
7714 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7715 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7716 {
7717 self.advance();
7718 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7719 {
7720 self.advance();
7721 Some(self.parse_expr(0)?)
7722 } else {
7723 None
7724 };
7725 return Ok(PlPgSqlStmt::Exit { when });
7726 }
7727 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7728 // already-parsed Statement or a runtime-computed SQL string.
7729 // The disambiguator vs the extended-query-protocol `EXECUTE
7730 // <stmt_name>` (which is a top-level Statement, not a
7731 // plpgsql line) is that inside a DO block / trigger body the
7732 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7733 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7734 {
7735 self.advance();
7736 let sql = self.parse_expr(0)?;
7737 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7738 }
7739 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7740 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7741 {
7742 self.advance();
7743 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7744 {
7745 self.advance();
7746 Some(self.parse_expr(0)?)
7747 } else {
7748 None
7749 };
7750 return Ok(PlPgSqlStmt::Continue { when });
7751 }
7752 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7753 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7754 {
7755 self.advance();
7756 let condition = self.parse_expr(0)?;
7757 let loop_kw = self.expect_ident_like()?;
7758 if !loop_kw.eq_ignore_ascii_case("loop") {
7759 return Err(self.err(alloc::format!(
7760 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7761 )));
7762 }
7763 let body = self.parse_plpgsql_stmt_list_until_end()?;
7764 // Expect END LOOP.
7765 let end_kw = self.expect_ident_like()?;
7766 if !end_kw.eq_ignore_ascii_case("end") {
7767 return Err(self.err(alloc::format!(
7768 "expected END LOOP after WHILE body, got {end_kw:?}"
7769 )));
7770 }
7771 let loop_kw2 = self.expect_ident_like()?;
7772 if !loop_kw2.eq_ignore_ascii_case("loop") {
7773 return Err(self.err(alloc::format!(
7774 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7775 )));
7776 }
7777 return Ok(PlPgSqlStmt::While { condition, body });
7778 }
7779 // v7.12.6 — RAISE.
7780 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7781 {
7782 self.advance();
7783 return self.parse_plpgsql_raise();
7784 }
7785 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7786 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7787 {
7788 self.advance();
7789 let condition = self.parse_expr(0)?;
7790 let message = if matches!(self.peek(), Token::Comma) {
7791 self.advance();
7792 Some(self.parse_expr(0)?)
7793 } else {
7794 None
7795 };
7796 return Ok(PlPgSqlStmt::Assert { condition, message });
7797 }
7798 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7799 // "PERFORM is equivalent to SELECT but discards the
7800 // result." Side effects (function calls, RAISE inside
7801 // SQL functions, etc.) still execute. We desugar to
7802 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7803 // existing embedded-statement path handles execution +
7804 // result-discard cleanly. The result is naturally
7805 // discarded because EmbeddedSql doesn't propagate row
7806 // sets back to the plpgsql interpreter.
7807 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7808 {
7809 self.advance();
7810 // Splice a synthetic Token::Select into the stream at
7811 // the current position so parse_select_stmt parses the
7812 // remainder as a normal SELECT body. Token-stream
7813 // surgery mirrors the try_parse_plpgsql_select_into
7814 // pattern used for SELECT … INTO desugaring.
7815 self.tokens.insert(self.pos, Token::Select);
7816 let select = self.parse_select_stmt()?;
7817 let Statement::Select(s) = select else {
7818 return Err(self.err(alloc::format!(
7819 "expected SELECT body after PERFORM, got {:?}",
7820 self.peek()
7821 )));
7822 };
7823 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7824 }
7825 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7826 // plpgsql-specific shape (mailrs round-10 migrate-042).
7827 // PG's SELECT INTO at top-level SQL would CREATE a new
7828 // table; inside plpgsql it ASSIGNS the query result to
7829 // a local variable. We detect the INTO at paren-depth
7830 // 0 between SELECT and the statement boundary; if
7831 // found, split the token stream into "pre-INTO
7832 // projection" + "var" + "post-INTO FROM/WHERE…" and
7833 // rebuild as a SelectInto with a regular SELECT body
7834 // (no INTO clause).
7835 if matches!(self.peek(), Token::Select)
7836 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7837 {
7838 return Ok(PlPgSqlStmt::SelectInto {
7839 var: var_name,
7840 body: Box::new(select_body),
7841 });
7842 }
7843 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7844 // SELECT can appear directly inside a trigger body; we
7845 // recurse into the regular Statement parser, which will
7846 // stop at the trailing `;` (which our caller then
7847 // consumes).
7848 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7849 // also embed ALTER / CREATE / DROP statements; route
7850 // those through the same parser so the DO body parses
7851 // cleanly.
7852 if matches!(self.peek(), Token::Insert)
7853 || matches!(self.peek(), Token::Select)
7854 || matches!(self.peek(), Token::Create)
7855 || matches!(self.peek(), Token::Drop)
7856 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7857 if s.eq_ignore_ascii_case("update")
7858 || s.eq_ignore_ascii_case("delete")
7859 || s.eq_ignore_ascii_case("alter"))
7860 {
7861 let stmt = self.parse_one_statement()?;
7862 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7863 }
7864 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7865 // followed by `:=` and an expression.
7866 let target = self.parse_plpgsql_assign_target()?;
7867 // PL/pgSQL assignment uses `:=`. The lexer represents
7868 // this as a colon followed by `=`; check both shapes.
7869 match self.peek() {
7870 Token::ColonEq => {
7871 self.advance();
7872 }
7873 Token::Colon => {
7874 self.advance();
7875 if !matches!(self.peek(), Token::Eq) {
7876 return Err(self.err(alloc::format!(
7877 "expected := after plpgsql assign target, got `:` then {:?}",
7878 self.peek()
7879 )));
7880 }
7881 self.advance();
7882 }
7883 other => {
7884 return Err(self.err(alloc::format!(
7885 "expected := after plpgsql assign target, got {other:?}"
7886 )));
7887 }
7888 }
7889 let value = self.parse_expr(0)?;
7890 Ok(PlPgSqlStmt::Assign { target, value })
7891 }
7892
7893 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7894 /// [ELSE body] END IF`. `IF` keyword already consumed.
7895 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7896 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7897 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7898 loop {
7899 // <expr> THEN
7900 let cond = self.parse_expr(0)?;
7901 let then_kw = self.expect_ident_like()?;
7902 if !then_kw.eq_ignore_ascii_case("then") {
7903 return Err(self.err(alloc::format!(
7904 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7905 )));
7906 }
7907 let body = self.parse_plpgsql_stmt_list_until_end()?;
7908 branches.push((cond, body));
7909 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7910 match self.peek() {
7911 Token::Ident(s) | Token::QuotedIdent(s)
7912 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7913 {
7914 self.advance();
7915 continue;
7916 }
7917 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7918 self.advance();
7919 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7920 break;
7921 }
7922 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7923 break;
7924 }
7925 other => {
7926 return Err(self.err(alloc::format!(
7927 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7928 )));
7929 }
7930 }
7931 }
7932 // Expect `END IF` (the END keyword is the one we're
7933 // looking at right now).
7934 let end_kw = self.expect_ident_like()?;
7935 if !end_kw.eq_ignore_ascii_case("end") {
7936 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7937 }
7938 let if_kw = self.expect_ident_like()?;
7939 if !if_kw.eq_ignore_ascii_case("if") {
7940 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7941 }
7942 Ok(PlPgSqlStmt::If {
7943 branches,
7944 else_branch,
7945 })
7946 }
7947
7948 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7949 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7950 /// is already consumed.
7951 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7952 let lvl_ident = self.expect_ident_like()?;
7953 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7954 "notice" => RaiseLevel::Notice,
7955 "warning" => RaiseLevel::Warning,
7956 "info" => RaiseLevel::Info,
7957 "log" => RaiseLevel::Log,
7958 "debug" => RaiseLevel::Debug,
7959 "exception" => RaiseLevel::Exception,
7960 other => {
7961 return Err(self.err(alloc::format!(
7962 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7963 )));
7964 }
7965 };
7966 // Message: required for v7.12.6. PG accepts a bare
7967 // RAISE-rethrow form (no message), reserved for future
7968 // RAISE-no-args support.
7969 let Token::String(msg) = self.peek() else {
7970 return Err(self.err(alloc::format!(
7971 "expected RAISE message string, got {:?}",
7972 self.peek()
7973 )));
7974 };
7975 let message = msg.clone();
7976 self.advance();
7977 // Optional comma-separated args (PG `%` format substitution).
7978 let mut args: Vec<Expr> = Vec::new();
7979 while matches!(self.peek(), Token::Comma) {
7980 self.advance();
7981 args.push(self.parse_expr(0)?);
7982 }
7983 Ok(PlPgSqlStmt::Raise {
7984 level,
7985 message,
7986 args,
7987 })
7988 }
7989
7990 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7991 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7992 /// migrate-042). Returns `(rebuilt_select_without_into,
7993 /// var_name)` when the pattern matches; `None` for
7994 /// regular SELECTs (those go through the embedded-SQL
7995 /// path). Token-stream surgery so the rebuilt SELECT
7996 /// parses through the regular `parse_select_stmt`.
7997 #[allow(clippy::too_many_lines)]
7998 fn try_parse_plpgsql_select_into(
7999 &mut self,
8000 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
8001 // Scan forward from `self.pos + 1` (past Token::Select)
8002 // for Token::Into at paren-depth 0, stopping at the
8003 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
8004 // end the plpgsql statement.
8005 let start = self.pos;
8006 let mut into_pos: Option<usize> = None;
8007 let mut depth: i32 = 0;
8008 let mut i = start + 1;
8009 while i < self.tokens.len() {
8010 match &self.tokens[i] {
8011 Token::LParen => depth += 1,
8012 Token::RParen => depth -= 1,
8013 Token::Semicolon if depth == 0 => break,
8014 Token::Ident(s)
8015 if depth == 0
8016 && (s.eq_ignore_ascii_case("end")
8017 || s.eq_ignore_ascii_case("else")
8018 || s.eq_ignore_ascii_case("elsif")) =>
8019 {
8020 break;
8021 }
8022 Token::Into if depth == 0 => {
8023 into_pos = Some(i);
8024 break;
8025 }
8026 _ => {}
8027 }
8028 i += 1;
8029 }
8030 let Some(into_at) = into_pos else {
8031 return Ok(None);
8032 };
8033 // The token immediately after INTO must be the target
8034 // var ident; anything else (e.g. INSERT INTO table)
8035 // ruled out by the depth-0 check above. Capture it.
8036 let var = match self.tokens.get(into_at + 1) {
8037 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8038 other => {
8039 return Err(self.err(alloc::format!(
8040 "expected variable name after SELECT … INTO, got {other:?}"
8041 )));
8042 }
8043 };
8044 // Find the end of the plpgsql SELECT INTO statement —
8045 // same boundary rules as the depth-0 scan above.
8046 let mut end = into_at + 2;
8047 let mut depth2: i32 = 0;
8048 while end < self.tokens.len() {
8049 match &self.tokens[end] {
8050 Token::LParen => depth2 += 1,
8051 Token::RParen => depth2 -= 1,
8052 Token::Semicolon if depth2 == 0 => break,
8053 Token::Ident(s)
8054 if depth2 == 0
8055 && (s.eq_ignore_ascii_case("end")
8056 || s.eq_ignore_ascii_case("else")
8057 || s.eq_ignore_ascii_case("elsif")) =>
8058 {
8059 break;
8060 }
8061 _ => {}
8062 }
8063 end += 1;
8064 }
8065 // Rebuild a token stream that represents the SELECT
8066 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8067 // post-var tokens up to statement end]. Run the
8068 // regular `parse_select_stmt` against it.
8069 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8070 for j in start..into_at {
8071 rebuilt.push(self.tokens[j].clone());
8072 }
8073 for j in (into_at + 2)..end {
8074 rebuilt.push(self.tokens[j].clone());
8075 }
8076 rebuilt.push(Token::Eof);
8077 let saved_pos = self.pos;
8078 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8079 self.pos = 0;
8080 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8081 if !matches!(self.peek(), Token::Select) {
8082 self.tokens = saved_tokens;
8083 self.pos = saved_pos;
8084 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8085 }
8086 let sel = self.parse_select_stmt();
8087 self.tokens = saved_tokens;
8088 self.pos = end;
8089 let sel = sel?;
8090 let Statement::Select(body) = sel else {
8091 return Err(self.err(alloc::format!(
8092 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8093 )));
8094 };
8095 Ok(Some((body, var)))
8096 }
8097
8098 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8099 // v7.16.1 — read the head token DIRECTLY rather than
8100 // via `expect_ident_like`. The v7.14.0 schema-qualifier
8101 // strip (`public.t` → `t`) inside `expect_ident_like`
8102 // greedily consumes any `ident . ident` pair, which
8103 // silently turned every `NEW.col := …` /
8104 // `OLD.col := …` plpgsql assignment into a Local("col")
8105 // assignment — the head "new"/"old" was eaten as if it
8106 // were a schema name and the Dot was consumed too, so
8107 // this function's own `peek() == Token::Dot` check
8108 // below never fired. Every BEFORE trigger that rewrote
8109 // a NEW cell was a silent no-op for two major releases
8110 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8111 // gate failures were investigated as v7.16.1 backlog.
8112 let head = match self.advance() {
8113 Token::Ident(s) | Token::QuotedIdent(s) => s,
8114 other => {
8115 return Err(self.err(alloc::format!(
8116 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8117 )));
8118 }
8119 };
8120 if matches!(self.peek(), Token::Dot) {
8121 self.advance();
8122 let col = self.expect_ident_like()?;
8123 if head.eq_ignore_ascii_case("new") {
8124 return Ok(AssignTarget::NewColumn(col));
8125 }
8126 if head.eq_ignore_ascii_case("old") {
8127 return Ok(AssignTarget::OldColumn(col));
8128 }
8129 return Err(self.err(alloc::format!(
8130 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8131 got {head:?}.<col>"
8132 )));
8133 }
8134 Ok(AssignTarget::Local(head))
8135 }
8136
8137 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8138 // RETURN NEW / OLD / NULL — bare-ident forms.
8139 match self.peek() {
8140 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8141 self.advance();
8142 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8143 }
8144 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8145 self.advance();
8146 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8147 }
8148 Token::Null => {
8149 self.advance();
8150 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8151 }
8152 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8153 // per PL/pgSQL convention.
8154 Token::Semicolon => {
8155 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8156 }
8157 _ => {}
8158 }
8159 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8160 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8161 // caller-visible effect (blocks don't return sets), so we
8162 // desugar it identically to PERFORM: parse the SELECT (or
8163 // EXECUTE dynamic) as embedded SQL that runs for side
8164 // effects and discards the result. RETURN NEXT <expr>
8165 // (single-row accumulator) queues with v7.40 SETOF function
8166 // infrastructure.
8167 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8168 // and keep going.
8169 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8170 {
8171 self.advance();
8172 let e = self.parse_expr(0)?;
8173 return Ok(PlPgSqlStmt::ReturnNext(e));
8174 }
8175 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8176 {
8177 self.advance();
8178 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8179 // rows go to the set, like the static form. It used to desugar to a
8180 // bare ExecuteDynamic, whose result was DISCARDED.
8181 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8182 {
8183 self.advance();
8184 let sql = self.parse_expr(0)?;
8185 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8186 }
8187 // Bare RETURN QUERY <select>. If the current token is
8188 // not already SELECT (e.g., the user wrote `RETURN QUERY
8189 // <projection> FROM ...` in a shorthand — rare but PG
8190 // accepts a bare projection here), splice one in. Same
8191 // trick as PERFORM.
8192 if !matches!(self.peek(), Token::Select) {
8193 self.tokens.insert(self.pos, Token::Select);
8194 }
8195 let select = self.parse_select_stmt()?;
8196 let Statement::Select(s) = select else {
8197 return Err(self.err(alloc::format!(
8198 "expected SELECT body after RETURN QUERY, got {:?}",
8199 self.peek()
8200 )));
8201 };
8202 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8203 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8204 // in a SETOF function is the entire answer thrown away.
8205 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8206 }
8207 // Fall through: parse a full expression.
8208 let e = self.parse_expr(0)?;
8209 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8210 }
8211
8212 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8213 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8214 // are ident-shaped (the parser keys off case-insensitive
8215 // match — same shape used by the top-level Update / Delete
8216 // dispatchers at parse_one_statement).
8217 if matches!(self.peek(), Token::Insert) {
8218 self.advance();
8219 return Ok(TriggerEvent::Insert);
8220 }
8221 match self.peek() {
8222 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8223 self.advance();
8224 Ok(TriggerEvent::Update)
8225 }
8226 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8227 self.advance();
8228 Ok(TriggerEvent::Delete)
8229 }
8230 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8231 self.advance();
8232 Ok(TriggerEvent::Truncate)
8233 }
8234 other => Err(self.err(alloc::format!(
8235 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8236 ))),
8237 }
8238 }
8239
8240 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8241 /// - (no clause) → implicit `FOR ALL TABLES`
8242 /// - `FOR ALL TABLES`
8243 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8244 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8245 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8246 /// REJECTS the bare plural (`invalid publication object list`,
8247 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8248 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8249 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8250 let name = self.expect_ident_or_string()?;
8251 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8252 // shape so existing publications keep parsing identically.
8253 let scope = if matches!(self.peek(), Token::For) {
8254 self.advance();
8255 if matches!(self.peek(), Token::All) {
8256 self.advance();
8257 if !matches!(self.peek(), Token::Tables) {
8258 return Err(self.err(format!(
8259 "expected TABLES after FOR ALL, got {:?}",
8260 self.peek()
8261 )));
8262 }
8263 self.advance();
8264 if matches!(self.peek(), Token::Except) {
8265 self.advance();
8266 let tables = self.parse_publication_table_list()?;
8267 PublicationScope::AllTablesExcept(tables)
8268 } else {
8269 PublicationScope::AllTables
8270 }
8271 } else if matches!(self.peek(), Token::Table) {
8272 self.advance();
8273 let tables = self.parse_publication_table_list()?;
8274 PublicationScope::ForTables(tables)
8275 } else if matches!(self.peek(), Token::Tables) {
8276 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8277 // plural (`FOR TABLES t`) is REJECTED (`invalid
8278 // publication object list`); TABLES only pairs with
8279 // `IN SCHEMA`. The old arm accepted it on an
8280 // unverifiable "PG 19 accepts both" claim.
8281 self.advance();
8282 if !matches!(self.peek(), Token::In) {
8283 return Err(self.err(alloc::string::String::from(
8284 "invalid publication object list",
8285 )));
8286 }
8287 self.advance();
8288 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8289 return Err(self.err(format!(
8290 "expected SCHEMA after FOR TABLES IN, got {:?}",
8291 self.peek()
8292 )));
8293 }
8294 self.advance();
8295 let schema = self.expect_ident_or_string()?;
8296 PublicationScope::TablesInSchema(schema)
8297 } else {
8298 return Err(self.err(format!(
8299 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8300 self.peek()
8301 )));
8302 }
8303 } else {
8304 PublicationScope::AllTables
8305 };
8306 Ok(Statement::CreatePublication(CreatePublicationStatement {
8307 name,
8308 scope,
8309 }))
8310 }
8311
8312 /// v6.1.3 — Comma-separated identifier list for the publication
8313 /// FOR-clause. Requires at least one entry; empty list is a
8314 /// parse error (PG behaviour). Quoted idents are accepted; the
8315 /// names round-trip through `Display` as `quote_ident(name)`.
8316 ///
8317 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8318 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8319 /// pg_dump output. SPG's publication state today is per-table
8320 /// only (matching the pre-PG-15 surface); the col list + WHERE
8321 /// are parsed so dumps load through and the table name reaches
8322 /// `PublicationScope::ForTables`, but the filter is not enforced
8323 /// at publish time. Re-open when a customer dogfood gate
8324 /// requires per-row-filter or column-subset publish semantics
8325 /// (which gates on persistent slot state landing first, 21.12).
8326 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8327 let first = self.parse_publication_table_entry()?;
8328 let mut out = alloc::vec![first];
8329 while matches!(self.peek(), Token::Comma) {
8330 self.advance();
8331 out.push(self.parse_publication_table_entry()?);
8332 }
8333 Ok(out)
8334 }
8335
8336 /// One table entry inside a FOR TABLE clause:
8337 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8338 /// Returns just the table name; the column list + WHERE predicate
8339 /// are consumed and discarded per the parse-accept-discard
8340 /// commitment above.
8341 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8342 let name = self.expect_ident_like()?;
8343 // Optional column list — `(col, col, …)`.
8344 if matches!(self.peek(), Token::LParen) {
8345 self.advance();
8346 // Empty parens are a PG error too; require ≥ 1 column.
8347 let _ = self.expect_ident_like()?;
8348 while matches!(self.peek(), Token::Comma) {
8349 self.advance();
8350 let _ = self.expect_ident_like()?;
8351 }
8352 if !matches!(self.peek(), Token::RParen) {
8353 return Err(self.err(alloc::format!(
8354 "expected ')' to close publication column list, got {:?}",
8355 self.peek()
8356 )));
8357 }
8358 self.advance();
8359 }
8360 // Optional row filter — `WHERE (predicate)`.
8361 if matches!(self.peek(), Token::Where) {
8362 self.advance();
8363 if !matches!(self.peek(), Token::LParen) {
8364 return Err(self.err(alloc::format!(
8365 "expected '(' after WHERE in publication row filter, got {:?}",
8366 self.peek()
8367 )));
8368 }
8369 self.advance();
8370 let _ = self.parse_expr(0)?;
8371 if !matches!(self.peek(), Token::RParen) {
8372 return Err(self.err(alloc::format!(
8373 "expected ')' to close publication WHERE filter, got {:?}",
8374 self.peek()
8375 )));
8376 }
8377 self.advance();
8378 }
8379 Ok(name)
8380 }
8381
8382 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8383 /// CONNECTION '<conn>'
8384 /// PUBLICATION <pub> [, <pub> ...]`.
8385 ///
8386 /// The clause order is fixed (CONNECTION first, then
8387 /// PUBLICATION) to match PG. No WITH-options accepted in
8388 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8389 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8390 let name = self.expect_ident_or_string()?;
8391 if !matches!(self.peek(), Token::Connection) {
8392 return Err(self.err(format!(
8393 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8394 self.peek()
8395 )));
8396 }
8397 self.advance();
8398 let conn_str = self.expect_string_literal()?;
8399 if !matches!(self.peek(), Token::Publication) {
8400 return Err(self.err(format!(
8401 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8402 self.peek()
8403 )));
8404 }
8405 self.advance();
8406 // Reuse the publication FOR-list parser shape: at least one
8407 // identifier, comma-separated.
8408 let first = self.expect_ident_like()?;
8409 let mut publications = alloc::vec![first];
8410 while matches!(self.peek(), Token::Comma) {
8411 self.advance();
8412 publications.push(self.expect_ident_like()?);
8413 }
8414 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8415 name,
8416 conn_str,
8417 publications,
8418 }))
8419 }
8420
8421 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8422 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8423 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8424 /// that fit `u64`.
8425 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8426 /// qualifier is a *namespace* the app owns (`app.user_id`,
8427 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8428 /// to discard. So parse the raw segments here instead of
8429 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8430 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8431 /// a single segment and round-trip unchanged.
8432 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8433 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8434 loop {
8435 let seg = match self.advance() {
8436 Token::Ident(s) | Token::QuotedIdent(s) => s,
8437 other if unreserved_keyword_text(&other).is_some() => {
8438 unreserved_keyword_text(&other).unwrap()
8439 }
8440 other => {
8441 return Err(ParseError {
8442 message: format!("expected parameter name, got {other:?}"),
8443 token_pos: self.consumed_pos(),
8444 });
8445 }
8446 };
8447 parts.push(seg);
8448 if matches!(self.peek(), Token::Dot) {
8449 self.advance();
8450 continue;
8451 }
8452 break;
8453 }
8454 Ok(parts.join(".").to_ascii_lowercase())
8455 }
8456
8457 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8458 Self::parse_set_value_inner(self)
8459 }
8460
8461 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8462 match self.advance() {
8463 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8464 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8465 Ok(crate::ast::SetValue::Default)
8466 }
8467 Token::Ident(s) | Token::QuotedIdent(s) => {
8468 let mut accum = s;
8469 while matches!(self.peek(), Token::Dot) {
8470 self.advance();
8471 let next = self.expect_ident_like()?;
8472 accum.push('.');
8473 accum.push_str(&next);
8474 }
8475 Ok(crate::ast::SetValue::Ident(accum))
8476 }
8477 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8478 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8479 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8480 // spellings that lex as keyword tokens, not idents:
8481 // `SET standard_conforming_strings = on` is in every
8482 // pg_dump preamble (`off` already lexes as an ident).
8483 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8484 // DEFAULT lexes as its keyword token, so the ident arm above
8485 // never saw it and the everyday reset form was a syntax error.
8486 Token::Default => Ok(crate::ast::SetValue::Default),
8487 // v7.40.11 — MySQL 9.7.2 accepts `NULL` here for exactly one
8488 // variable and rejects it with error 1231 for every other
8489 // (both measured); PostgreSQL 18.6 rejects the token itself
8490 // with `syntax error at or near "NULL"`. So the token is
8491 // admitted only for a MySQL session and the per-variable
8492 // decision is the executor's.
8493 Token::Null if self.mysql_dialect => Ok(crate::ast::SetValue::Null),
8494 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8495 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8496 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8497 // v7.14.0 — MySQL session/user variable RHS
8498 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8499 // Wrap as Ident so the SET handler can record it; the
8500 // engine treats `@VAR` / `@@VAR` values as opaque
8501 // strings.
8502 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8503 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8504 // is the common MySQL preamble shape. Allow a `+` or
8505 // `-` prefix on negative numerics for parity with PG
8506 // (some param defaults are negative).
8507 Token::Minus => match self.advance() {
8508 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8509 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8510 other => Err(self.err(format!(
8511 "expected numeric after `-` in SET value, got {other:?}"
8512 ))),
8513 },
8514 other => Err(self.err(format!(
8515 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8516 ))),
8517 }
8518 }
8519
8520 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8521 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8522 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8523 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8524 /// present). Modes are comma-separated per PG; SPG also
8525 /// accepts space-separated for tolerance. READ ONLY / WRITE
8526 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8527 /// surface but not behaviorally honoured today).
8528 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8529 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8530 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8531 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8532 /// session default rather than forcing READ COMMITTED.
8533 fn parse_isolation_level_clauses(
8534 &mut self,
8535 ) -> Result<crate::ast::TransactionModes, ParseError> {
8536 let mut level = IsolationLevel::default();
8537 let mut have_level = false;
8538 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8539 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8540 let mut read_only: Option<bool> = None;
8541 loop {
8542 // ISOLATION LEVEL …
8543 let saw_isolation =
8544 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8545 if saw_isolation {
8546 self.advance(); // ISOLATION
8547 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8548 return Err(self.err(alloc::format!(
8549 "expected LEVEL after ISOLATION, got {:?}",
8550 self.peek()
8551 )));
8552 }
8553 self.advance(); // LEVEL
8554 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8555 let w1 = self
8556 .expect_ident_like()
8557 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8558 let lc = w1.to_ascii_lowercase();
8559 level = match lc.as_str() {
8560 "serializable" => IsolationLevel::Serializable,
8561 "repeatable" => {
8562 // Expect READ
8563 let w2 = self
8564 .expect_ident_like()
8565 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8566 if !w2.eq_ignore_ascii_case("read") {
8567 return Err(self.err(alloc::format!(
8568 "expected READ after REPEATABLE, got {w2:?}"
8569 )));
8570 }
8571 IsolationLevel::RepeatableRead
8572 }
8573 "read" => {
8574 let w2 = self
8575 .expect_ident_like()
8576 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8577 match w2.to_ascii_lowercase().as_str() {
8578 "committed" => IsolationLevel::ReadCommitted,
8579 "uncommitted" => IsolationLevel::ReadUncommitted,
8580 other => {
8581 return Err(self.err(alloc::format!(
8582 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8583 )));
8584 }
8585 }
8586 }
8587 other => {
8588 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8589 }
8590 };
8591 have_level = true;
8592 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8593 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8594 // read "parsed, not behaviorally honoured", and it was
8595 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8596 // opened an ordinary read-write transaction and accepted
8597 // every write in it.
8598 self.advance();
8599 match self.peek().clone() {
8600 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8601 self.advance();
8602 read_only = Some(true);
8603 }
8604 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8605 self.advance();
8606 read_only = Some(false);
8607 }
8608 other => {
8609 return Err(self.err(alloc::format!(
8610 "expected ONLY or WRITE after READ, got {other:?}"
8611 )));
8612 }
8613 }
8614 } else if matches!(self.peek(), Token::Not) {
8615 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8616 self.advance();
8617 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8618 return Err(self.err(alloc::format!(
8619 "expected DEFERRABLE after NOT, got {:?}",
8620 self.peek()
8621 )));
8622 }
8623 self.advance();
8624 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8625 {
8626 self.advance();
8627 } else {
8628 break;
8629 }
8630 // Optional comma between modes.
8631 if matches!(self.peek(), Token::Comma) {
8632 self.advance();
8633 }
8634 }
8635 Ok(crate::ast::TransactionModes {
8636 isolation: have_level.then_some(level),
8637 read_only,
8638 })
8639 }
8640
8641 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8642 // FOR is a v6.1.2-reserved keyword (Token::For). The
8643 // other two are bare idents — they've never needed lexer
8644 // support and we keep it that way.
8645 if !matches!(self.peek(), Token::For) {
8646 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8647 }
8648 self.advance();
8649 self.expect_keyword_ident("wal")?;
8650 self.expect_keyword_ident("position")?;
8651 let pos = self.expect_u64_literal()?;
8652 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8653 {
8654 self.advance();
8655 self.expect_keyword_ident("timeout")?;
8656 Some(self.expect_u64_literal()?)
8657 } else {
8658 None
8659 };
8660 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8661 }
8662
8663 /// v6.1.7 helper — consume a `Token::Integer` and check it
8664 /// fits `u64`. WAL positions and millisecond timeouts are
8665 /// non-negative.
8666 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8667 match self.advance() {
8668 Token::Integer(n) if n >= 0 => Ok(n as u64),
8669 Token::Integer(n) => Err(ParseError {
8670 message: format!("expected non-negative integer, got {n}"),
8671 token_pos: self.consumed_pos(),
8672 }),
8673 other => Err(ParseError {
8674 message: format!("expected integer literal, got {other:?}"),
8675 token_pos: self.consumed_pos(),
8676 }),
8677 }
8678 }
8679
8680 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8681 /// ROLE '<role>' (defaults to readonly). All string slots accept
8682 /// either a quoted ident or a quoted string literal.
8683 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8684 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8685 ///
8686 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8687 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8688 /// wire role) still parses — it is a different axis from the PG attributes.
8689 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8690 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8691 /// or RESET, so the plain attribute forms keep their old path.
8692 fn peeks_db_role_setting(&self) -> bool {
8693 let mut i = self.pos + 1; // past the object's name
8694 let word = |p: usize| -> Option<String> {
8695 match self.tokens.get(p) {
8696 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8697 Some(Token::In) => Some(String::from("in")),
8698 _ => None,
8699 }
8700 };
8701 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8702 i += 3; // IN DATABASE <name>
8703 }
8704 matches!(word(i).as_deref(), Some("set" | "reset"))
8705 }
8706
8707 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8708 use crate::ast::SetDbRoleSettingStatement;
8709 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8710 // identifier, so the ordinary name reader refuses it. Same trap
8711 // as TABLE / INDEX / FULL / DEFAULT before it.
8712 let name = if matches!(self.peek(), Token::All) {
8713 self.advance();
8714 String::from("all")
8715 } else {
8716 self.expect_ident_or_string()?
8717 };
8718 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8719 let all = name.eq_ignore_ascii_case("all");
8720 let (mut database, mut role) = if is_database {
8721 (Some(name), None)
8722 } else if all {
8723 (None, None)
8724 } else {
8725 (None, Some(name))
8726 };
8727 if matches!(self.peek(), Token::In) {
8728 self.advance();
8729 self.advance(); // DATABASE
8730 database = Some(self.expect_ident_or_string()?);
8731 }
8732 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8733 self.advance(); // SET | RESET
8734 if resetting && matches!(self.peek(), Token::All) {
8735 self.advance();
8736 self.consume_until_statement_boundary();
8737 return Ok(Statement::SetDbRoleSetting(Box::new(
8738 SetDbRoleSettingStatement {
8739 database,
8740 role,
8741 param: None,
8742 value: None,
8743 },
8744 )));
8745 }
8746 let param = self.expect_ident_like()?;
8747 let value = if resetting {
8748 None
8749 } else {
8750 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8751 // KEYWORD, so the ident-only check missed it and consumed
8752 // the word itself as the value — the same trap as ALL, one
8753 // clause over.
8754 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8755 self.advance();
8756 }
8757 Some(self.take_guc_value())
8758 };
8759 self.consume_until_statement_boundary();
8760 Ok(Statement::SetDbRoleSetting(Box::new(
8761 SetDbRoleSettingStatement {
8762 database,
8763 role,
8764 param: Some(param),
8765 value,
8766 },
8767 )))
8768 }
8769
8770 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8771 /// a quoted literal loses its quotes, a bare word or number does not.
8772 fn take_guc_value(&mut self) -> String {
8773 match self.advance() {
8774 Token::String(s) => s,
8775 Token::Integer(n) => format!("{n}"),
8776 Token::Float(f) => format!("{f}"),
8777 Token::Ident(s) | Token::QuotedIdent(s) => s,
8778 other => format!("{other:?}"),
8779 }
8780 }
8781
8782 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8783 let name = self.expect_ident_or_string()?;
8784 if self.peek_keyword_ident("with") {
8785 self.advance();
8786 }
8787 let mut password = String::new();
8788 let mut role = String::new();
8789 let mut login: Option<bool> = None;
8790 let mut inherit: Option<bool> = None;
8791 let mut superuser: Option<bool> = None;
8792 // Not a `while let`: the pattern would borrow `self` across the
8793 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8794 #[allow(clippy::while_let_loop)]
8795 loop {
8796 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8797 break;
8798 };
8799 match w.to_ascii_lowercase().as_str() {
8800 "password" => {
8801 self.advance();
8802 password = self.expect_string_literal()?;
8803 }
8804 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8805 // is the same slot.
8806 "encrypted" => {
8807 self.advance();
8808 self.expect_keyword_ident("password")?;
8809 password = self.expect_string_literal()?;
8810 }
8811 "login" => {
8812 self.advance();
8813 login = Some(true);
8814 }
8815 "nologin" => {
8816 self.advance();
8817 login = Some(false);
8818 }
8819 "inherit" => {
8820 self.advance();
8821 inherit = Some(true);
8822 }
8823 "noinherit" => {
8824 self.advance();
8825 inherit = Some(false);
8826 }
8827 "superuser" => {
8828 self.advance();
8829 superuser = Some(true);
8830 }
8831 "nosuperuser" => {
8832 self.advance();
8833 superuser = Some(false);
8834 }
8835 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8836 "role" => {
8837 self.advance();
8838 role = self.expect_string_literal()?;
8839 }
8840 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8841 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8842 // accepted and ignored so a pg_dump role block restores. They
8843 // gate capabilities SPG does not have.
8844 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8845 | "noreplication" | "bypassrls" | "nobypassrls" => {
8846 self.advance();
8847 }
8848 "connection" => {
8849 self.advance();
8850 self.expect_keyword_ident("limit")?;
8851 self.advance(); // the number
8852 }
8853 "valid" => {
8854 self.advance();
8855 self.expect_keyword_ident("until")?;
8856 self.expect_string_literal()?;
8857 }
8858 _ => break,
8859 }
8860 }
8861 if role.is_empty() {
8862 role = "readonly".to_string();
8863 }
8864 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8865 name,
8866 password,
8867 role,
8868 login,
8869 inherit,
8870 superuser,
8871 is_user,
8872 }))
8873 }
8874
8875 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8876 /// consumed the USING / WITH CHECK keyword.
8877 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8878 if !matches!(self.peek(), Token::LParen) {
8879 return Err(self.err(alloc::format!(
8880 "expected '(' after {clause}, got {:?}",
8881 self.peek()
8882 )));
8883 }
8884 self.advance();
8885 let e = self.parse_expr(0)?;
8886 if !matches!(self.peek(), Token::RParen) {
8887 return Err(self.err(alloc::format!(
8888 "expected ')' to close {clause}, got {:?}",
8889 self.peek()
8890 )));
8891 }
8892 self.advance();
8893 Ok(e)
8894 }
8895
8896 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8897 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8898 let mut roles = Vec::new();
8899 loop {
8900 roles.push(self.expect_ident_like()?);
8901 if matches!(self.peek(), Token::Comma) {
8902 self.advance();
8903 } else {
8904 break;
8905 }
8906 }
8907 Ok(roles)
8908 }
8909
8910 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8911 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8912 /// `CREATE POLICY`.
8913 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8914 use crate::ast::PolicyCmd;
8915 let name = self.expect_ident_like()?;
8916 if !matches!(self.peek(), Token::On) {
8917 return Err(self.err(alloc::format!(
8918 "expected ON after CREATE POLICY name, got {:?}",
8919 self.peek()
8920 )));
8921 }
8922 self.advance();
8923 let table = self.expect_ident_like()?;
8924
8925 let mut permissive = true;
8926 if matches!(self.peek(), Token::As) {
8927 self.advance();
8928 let w = self.expect_ident_like()?;
8929 permissive = if w.eq_ignore_ascii_case("permissive") {
8930 true
8931 } else if w.eq_ignore_ascii_case("restrictive") {
8932 false
8933 } else {
8934 return Err(self.err(alloc::format!(
8935 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8936 )));
8937 };
8938 }
8939
8940 let mut cmd = PolicyCmd::All;
8941 if matches!(self.peek(), Token::For) {
8942 self.advance();
8943 cmd = self.parse_policy_cmd()?;
8944 }
8945
8946 let mut roles = Vec::new();
8947 if matches!(self.peek(), Token::To) {
8948 self.advance();
8949 roles = self.parse_policy_roles()?;
8950 }
8951
8952 let mut using = None;
8953 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8954 {
8955 self.advance();
8956 using = Some(self.parse_paren_expr("USING")?);
8957 }
8958
8959 let mut with_check = None;
8960 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8961 {
8962 self.advance();
8963 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8964 {
8965 return Err(self.err(alloc::format!(
8966 "expected CHECK after WITH, got {:?}",
8967 self.peek()
8968 )));
8969 }
8970 self.advance();
8971 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8972 }
8973
8974 // Clause-per-command matrix (PG wording).
8975 match cmd {
8976 PolicyCmd::Insert => {
8977 if using.is_some() {
8978 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8979 }
8980 }
8981 PolicyCmd::Select | PolicyCmd::Delete => {
8982 if with_check.is_some() {
8983 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8984 }
8985 }
8986 PolicyCmd::Update | PolicyCmd::All => {}
8987 }
8988
8989 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8990 name,
8991 table,
8992 permissive,
8993 cmd,
8994 roles,
8995 using,
8996 with_check,
8997 }))
8998 }
8999
9000 /// v7.39 (RLS) — the command word after `FOR`.
9001 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
9002 use crate::ast::PolicyCmd;
9003 match self.peek().clone() {
9004 Token::All => {
9005 self.advance();
9006 Ok(PolicyCmd::All)
9007 }
9008 Token::Select => {
9009 self.advance();
9010 Ok(PolicyCmd::Select)
9011 }
9012 Token::Insert => {
9013 self.advance();
9014 Ok(PolicyCmd::Insert)
9015 }
9016 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
9017 self.advance();
9018 Ok(PolicyCmd::Update)
9019 }
9020 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
9021 self.advance();
9022 Ok(PolicyCmd::Delete)
9023 }
9024 other => Err(self.err(alloc::format!(
9025 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
9026 ))),
9027 }
9028 }
9029
9030 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9031 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9032 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9033 let name = self.expect_ident_like()?;
9034 if !matches!(self.peek(), Token::On) {
9035 return Err(self.err(alloc::format!(
9036 "expected ON after ALTER POLICY name, got {:?}",
9037 self.peek()
9038 )));
9039 }
9040 self.advance();
9041 let table = self.expect_ident_like()?;
9042
9043 // RENAME TO new
9044 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9045 {
9046 self.advance();
9047 if !matches!(self.peek(), Token::To) {
9048 return Err(self.err(alloc::format!(
9049 "expected TO after RENAME, got {:?}",
9050 self.peek()
9051 )));
9052 }
9053 self.advance();
9054 let new = self.expect_ident_like()?;
9055 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9056 name,
9057 table,
9058 rename_to: Some(new),
9059 roles: None,
9060 using: None,
9061 with_check: None,
9062 }));
9063 }
9064
9065 let mut roles = None;
9066 if matches!(self.peek(), Token::To) {
9067 self.advance();
9068 roles = Some(self.parse_policy_roles()?);
9069 }
9070 let mut using = None;
9071 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9072 {
9073 self.advance();
9074 using = Some(self.parse_paren_expr("USING")?);
9075 }
9076 let mut with_check = None;
9077 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9078 {
9079 self.advance();
9080 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9081 {
9082 return Err(self.err(alloc::format!(
9083 "expected CHECK after WITH, got {:?}",
9084 self.peek()
9085 )));
9086 }
9087 self.advance();
9088 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9089 }
9090 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9091 name,
9092 table,
9093 rename_to: None,
9094 roles,
9095 using,
9096 with_check,
9097 }))
9098 }
9099
9100 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9101 /// `DROP POLICY`.
9102 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9103 let if_exists = self.consume_if_exists();
9104 let name = self.expect_ident_like()?;
9105 if !matches!(self.peek(), Token::On) {
9106 return Err(self.err(alloc::format!(
9107 "expected ON after DROP POLICY name, got {:?}",
9108 self.peek()
9109 )));
9110 }
9111 self.advance();
9112 let table = self.expect_ident_like()?;
9113 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9114 name,
9115 table,
9116 if_exists,
9117 }))
9118 }
9119}
9120fn wrap_from_leaves(
9121 e: &mut Expr,
9122 names: &[String],
9123 make: &dyn Fn(Expr) -> Expr,
9124 refs: &dyn Fn(&Expr) -> bool,
9125) {
9126 if let Expr::Column(c) = e {
9127 if c.qualifier
9128 .as_deref()
9129 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9130 {
9131 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9132 *e = make(taken);
9133 }
9134 return;
9135 }
9136 match e {
9137 Expr::Binary { lhs, rhs, .. } => {
9138 wrap_from_leaves(lhs, names, make, refs);
9139 wrap_from_leaves(rhs, names, make, refs);
9140 }
9141 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9142 wrap_from_leaves(expr, names, make, refs)
9143 }
9144 Expr::FunctionCall { args, .. } => {
9145 for a in args.iter_mut() {
9146 wrap_from_leaves(a, names, make, refs);
9147 }
9148 }
9149 Expr::Case {
9150 operand,
9151 branches,
9152 else_branch,
9153 } => {
9154 if let Some(o) = operand.as_deref_mut() {
9155 wrap_from_leaves(o, names, make, refs);
9156 }
9157 for (w, t) in branches.iter_mut() {
9158 wrap_from_leaves(w, names, make, refs);
9159 wrap_from_leaves(t, names, make, refs);
9160 }
9161 if let Some(el) = else_branch.as_deref_mut() {
9162 wrap_from_leaves(el, names, make, refs);
9163 }
9164 }
9165 // Compound variants the walk doesn't decompose: keep the
9166 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9167 // a source table, so nothing regresses.
9168 other => {
9169 if refs(other) {
9170 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9171 *other = make(taken);
9172 }
9173 }
9174 }
9175}
9176
9177/// v7.39 (round 241) — does this expression reference any of the FROM /
9178/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9179/// lowerings)?
9180fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9181 match e {
9182 Expr::Column(c) => c
9183 .qualifier
9184 .as_deref()
9185 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9186 Expr::Binary { lhs, rhs, .. } => {
9187 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9188 }
9189 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9190 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9191 Expr::Case {
9192 operand,
9193 branches,
9194 else_branch,
9195 } => {
9196 operand
9197 .as_deref()
9198 .is_some_and(|o| expr_refs_tables(o, names))
9199 || branches
9200 .iter()
9201 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9202 || else_branch
9203 .as_deref()
9204 .is_some_and(|el| expr_refs_tables(el, names))
9205 }
9206 _ => false,
9207 }
9208}
9209
9210impl Parser {
9211 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9212 /// Caller already consumed the leading `UPDATE` ident.
9213 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9214 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9215 /// after the target name has been read. `JOIN` is a reserved token;
9216 /// the qualifiers are bare idents.
9217 fn peek_is_update_join_start(&self) -> bool {
9218 match self.peek() {
9219 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9220 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9221 Token::Join
9222 | Token::Inner
9223 | Token::Left
9224 | Token::Right
9225 | Token::Cross
9226 | Token::Full => true,
9227 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9228 Token::Ident(s) | Token::QuotedIdent(s) => {
9229 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9230 }
9231 _ => false,
9232 }
9233 }
9234
9235 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9236 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9237 /// expression on the right, and `:=` as a second spelling of `=`.
9238 ///
9239 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9240 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9241 /// and holding this loop's `Vec` + `String` locals there overflowed the
9242 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9243 #[inline(never)]
9244 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9245 let mut assigns: Vec<(String, Expr)> = Vec::new();
9246 let mut settings: Vec<(String, Expr)> = Vec::new();
9247 loop {
9248 // v7.39 (round 554) — a plain NAME here is a session
9249 // setting, not a user variable. mysqldump writes the two in
9250 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9251 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9252 // changes it — and this refused the mixture outright, so no
9253 // dump could be restored past its preamble.
9254 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9255 self.advance();
9256 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9257 return Err(self.err(alloc::format!(
9258 "expected `=` after {name}, got {:?}",
9259 self.peek()
9260 )));
9261 }
9262 self.advance();
9263 let value = self.parse_expr(0)?;
9264 settings.push((name.to_ascii_lowercase(), value));
9265 if matches!(self.peek(), Token::Comma) {
9266 self.advance();
9267 continue;
9268 }
9269 break;
9270 }
9271 let Token::SessionVar(raw) = self.peek().clone() else {
9272 return Err(self.err(alloc::format!(
9273 "expected a user variable after SET, got {:?}",
9274 self.peek()
9275 )));
9276 };
9277 if raw.starts_with("@@") {
9278 return Err(self.err(alloc::string::String::from(
9279 "cannot mix `@@` settings with `@` user variables in one SET",
9280 )));
9281 }
9282 self.advance();
9283 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9284 return Err(self.err(alloc::format!(
9285 "expected `=` or `:=` after {raw}, got {:?}",
9286 self.peek()
9287 )));
9288 }
9289 self.advance();
9290 let value = self.parse_expr(0)?;
9291 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9292 if matches!(self.peek(), Token::Comma) {
9293 self.advance();
9294 continue;
9295 }
9296 break;
9297 }
9298 Ok(Statement::SetUserVars(assigns, settings))
9299 }
9300
9301 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9302 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9303 // NAMED `only` until now, which failed on `relation "only" does
9304 // not exist`. The lookahead is what keeps a table actually
9305 // called `only` working: the keyword is only a keyword when a
9306 // TABLE NAME follows it — and `SET` arrives as an identifier
9307 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9308 // for the table and die on the `=`. Measured by the pin.
9309 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9310 if s.eq_ignore_ascii_case("only"))
9311 && matches!(
9312 self.tokens.get(self.pos + 1),
9313 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9314 );
9315 if only {
9316 self.advance();
9317 }
9318 let table = self.expect_ident_like()?;
9319 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9320 // bare spelling; a bare identifier that is the SET keyword itself
9321 // is the clause, not an alias.
9322 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9323 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9324 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9325 // following JOIN a syntax error.
9326 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9327 let alias = if matches!(self.peek(), Token::As) {
9328 self.advance();
9329 Some(self.expect_ident_like()?)
9330 } else {
9331 match self.peek() {
9332 Token::Ident(s) | Token::QuotedIdent(s)
9333 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9334 {
9335 let a = s.clone();
9336 self.advance();
9337 Some(a)
9338 }
9339 _ => None,
9340 }
9341 };
9342 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9343 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9344 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9345 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9346 // The FIRST table is the mutation target and the rest are sources —
9347 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9348 // SPG already lowers onto correlated subqueries. So rewind, let
9349 // `parse_from_clause` read the whole list (it handles aliases, comma
9350 // lists, and every JOIN form), then peel the target off the front.
9351 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9352 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9353 {
9354 // NOTE: `advance()` destroys the tokens it returns
9355 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9356 // is NOT possible — the tail is read forward, once, through the
9357 // same grammar `parse_from_clause` uses after its primary.
9358 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9359 let mut joins = self.parse_from_joins(&target_qual)?;
9360 if joins.is_empty() {
9361 return Err(self.err(alloc::string::String::from(
9362 "multi-table UPDATE needs at least one source table",
9363 )));
9364 }
9365 let head = joins.remove(0);
9366 // A LEFT join keeps every target row (the unmatched ones see NULL
9367 // on the source side), so it must NOT get the EXISTS row filter
9368 // the inner / comma forms use.
9369 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9370 let src = FromClause {
9371 primary: head.table,
9372 joins,
9373 };
9374 (Some(src), head.on, outer)
9375 } else {
9376 (None, None, false)
9377 };
9378 self.expect_keyword_ident("set")?;
9379 let mut assignments = Vec::new();
9380 loop {
9381 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9382 // …)` — the parenthesized multi-assignment. Expressions
9383 // assign positionally; a subquery RHS clones per column
9384 // keeping only the Nth projection item.
9385 if matches!(self.peek(), Token::LParen) {
9386 self.advance();
9387 let mut cols = alloc::vec![self.expect_ident_like()?];
9388 while matches!(self.peek(), Token::Comma) {
9389 self.advance();
9390 cols.push(self.expect_ident_like()?);
9391 }
9392 if !matches!(self.peek(), Token::RParen) {
9393 return Err(self.err(format!(
9394 "expected ')' after SET column list, got {:?}",
9395 self.peek()
9396 )));
9397 }
9398 self.advance();
9399 if !matches!(self.peek(), Token::Eq) {
9400 return Err(self.err(format!(
9401 "expected `=` after SET column list, got {:?}",
9402 self.peek()
9403 )));
9404 }
9405 self.advance();
9406 if !matches!(self.peek(), Token::LParen) {
9407 return Err(self.err(format!(
9408 "expected '(' after SET (…) =, got {:?}",
9409 self.peek()
9410 )));
9411 }
9412 self.advance();
9413 if matches!(self.peek(), Token::Select) {
9414 let inner = match self.parse_select_stmt()? {
9415 Statement::Select(s) => s,
9416 other => {
9417 return Err(self.err(alloc::format!(
9418 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9419 )));
9420 }
9421 };
9422 if !matches!(self.peek(), Token::RParen) {
9423 return Err(self.err(format!(
9424 "expected ')' after SET subquery, got {:?}",
9425 self.peek()
9426 )));
9427 }
9428 self.advance();
9429 if inner.items.len() != cols.len() {
9430 return Err(self.err(alloc::format!(
9431 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9432 cols.len(),
9433 inner.items.len()
9434 )));
9435 }
9436 for (i, col) in cols.into_iter().enumerate() {
9437 let mut sub = inner.clone();
9438 sub.items = alloc::vec![sub.items[i].clone()];
9439 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9440 }
9441 } else {
9442 let mut exprs = alloc::vec![self.parse_expr(0)?];
9443 while matches!(self.peek(), Token::Comma) {
9444 self.advance();
9445 exprs.push(self.parse_expr(0)?);
9446 }
9447 if !matches!(self.peek(), Token::RParen) {
9448 return Err(self.err(format!(
9449 "expected ')' after SET row values, got {:?}",
9450 self.peek()
9451 )));
9452 }
9453 self.advance();
9454 if exprs.len() != cols.len() {
9455 return Err(self.err(alloc::format!(
9456 "SET (…) = (…) arity mismatch: {} columns, {} values",
9457 cols.len(),
9458 exprs.len()
9459 )));
9460 }
9461 for (col, e) in cols.into_iter().zip(exprs) {
9462 assignments.push((col, e));
9463 }
9464 }
9465 if matches!(self.peek(), Token::Comma) {
9466 self.advance();
9467 continue;
9468 }
9469 break;
9470 }
9471 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9472 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9473 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9474 // `public.` dump qualifiers), so the qualifier has to be read off
9475 // the token stream first — otherwise `SET b.v = 888` would write
9476 // to the TARGET table's `v` while naming a source table, a
9477 // silent-wrong. A qualifier naming a SOURCE table means a
9478 // multi-TARGET update — mutating two tables in one statement —
9479 // which SPG does not model, so it is refused loudly.
9480 let set_qual: Option<String> = if mysql_from.is_some()
9481 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9482 {
9483 match self.peek() {
9484 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9485 _ => None,
9486 }
9487 } else {
9488 None
9489 };
9490 let col = self.expect_ident_like()?;
9491 if let Some(q) = set_qual {
9492 let names_target = q.eq_ignore_ascii_case(&table)
9493 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9494 if !names_target {
9495 return Err(self.err(alloc::format!(
9496 "multi-table UPDATE can only assign to its first table \
9497 ({table}); `{q}.{col}` targets another table"
9498 )));
9499 }
9500 }
9501 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9502 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9503 // `__column_default` marker lowering just below). PG assigns to the
9504 // i-th (1-based) element, NULL-padding when i exceeds the length.
9505 if matches!(self.peek(), Token::LBracket) {
9506 self.advance();
9507 let index = self.parse_expr(0)?;
9508 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9509 // (and the open `arr[lo:]`), lowered to
9510 // `__array_assign_slice`. Only the single-subscript form
9511 // parsed before, so a slice assignment was a syntax error.
9512 let mut slice_hi: Option<Option<Expr>> = None;
9513 if matches!(self.peek(), Token::Colon) {
9514 self.advance();
9515 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9516 None
9517 } else {
9518 Some(self.parse_expr(0)?)
9519 });
9520 }
9521 if !matches!(self.peek(), Token::RBracket) {
9522 return Err(self.err(format!(
9523 "expected `]` after array subscript in UPDATE SET, got {:?}",
9524 self.peek()
9525 )));
9526 }
9527 self.advance();
9528 if !matches!(self.peek(), Token::Eq) {
9529 return Err(self.err(format!(
9530 "expected `=` after array subscript in UPDATE SET, got {:?}",
9531 self.peek()
9532 )));
9533 }
9534 self.advance();
9535 let value = self.parse_expr(0)?;
9536 // PG merges several subscript writes to the same column into one
9537 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9538 // assignment to `col` rather than each overwriting the original.
9539 let existing = assignments.iter().position(|(c, _)| c == &col);
9540 let base = match existing {
9541 Some(i) => assignments[i].1.clone(),
9542 None => Expr::Column(ColumnName {
9543 qualifier: None,
9544 name: col.clone(),
9545 }),
9546 };
9547 let assigned = match slice_hi {
9548 None => Expr::FunctionCall {
9549 name: "__array_assign".to_string(),
9550 args: alloc::vec![base, index, value],
9551 },
9552 Some(hi) => Expr::FunctionCall {
9553 name: "__array_assign_slice".to_string(),
9554 args: alloc::vec![
9555 base,
9556 index,
9557 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9558 value,
9559 ],
9560 },
9561 };
9562 match existing {
9563 Some(i) => assignments[i].1 = assigned,
9564 None => assignments.push((col, assigned)),
9565 }
9566 if matches!(self.peek(), Token::Comma) {
9567 self.advance();
9568 continue;
9569 }
9570 break;
9571 }
9572 if !matches!(self.peek(), Token::Eq) {
9573 return Err(self.err(format!(
9574 "expected `=` after column name in UPDATE SET, got {:?}",
9575 self.peek()
9576 )));
9577 }
9578 self.advance();
9579 // `SET col = DEFAULT` — the column's declared default;
9580 // rides out as a marker call the update executor
9581 // resolves against the schema.
9582 let value = if matches!(self.peek(), Token::Default) {
9583 self.advance();
9584 Expr::FunctionCall {
9585 name: "__column_default".to_string(),
9586 args: Vec::new(),
9587 }
9588 } else {
9589 self.parse_expr(0)?
9590 };
9591 assignments.push((col, value));
9592 if matches!(self.peek(), Token::Comma) {
9593 self.advance();
9594 continue;
9595 }
9596 break;
9597 }
9598 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9599 // update. Lowers onto the correlated-subquery machinery:
9600 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9601 // and each assignment that references a FROM-list table
9602 // wraps into a correlated scalar subquery
9603 // (SELECT expr FROM src WHERE cond). Equivalent for the
9604 // unique-join shape (the overwhelmingly common one); a
9605 // multi-match, which PG resolves by arbitrary pick,
9606 // surfaces as a scalar-subquery cardinality error instead
9607 // of a silent arbitrary result.
9608 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9609 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9610 // the SAME lowering below. Both spellings together is not legal in
9611 // either dialect.
9612 let from_clause = if let Some(fc) = mysql_from {
9613 if matches!(self.peek(), Token::From) {
9614 return Err(self.err(alloc::string::String::from(
9615 "multi-table UPDATE already names its sources; drop the FROM clause",
9616 )));
9617 }
9618 Some(fc)
9619 } else if matches!(self.peek(), Token::From) {
9620 self.advance();
9621 Some(self.parse_from_clause()?)
9622 } else {
9623 None
9624 };
9625 let where_ = if matches!(self.peek(), Token::Where) {
9626 self.advance();
9627 Some(self.parse_expr(0)?)
9628 } else {
9629 None
9630 };
9631 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9632 // and the TARGET-row filter are NOT the same predicate once a LEFT
9633 // join is involved:
9634 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9635 // one conjunction, and the whole thing filters target rows via
9636 // EXISTS.
9637 // * LEFT join: only the ON predicate belongs inside the source
9638 // subquery. The WHERE still filters TARGET rows (with source
9639 // columns read through the correlated subquery, which yields NULL
9640 // for an unmatched row — exactly LEFT-join semantics).
9641 // Round 420 folded ON into WHERE unconditionally and then dropped the
9642 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9643 // WHERE a.id > 1` updated EVERY row.
9644 let sub_where = match (mysql_on.clone(), where_.clone()) {
9645 _ if mysql_outer => mysql_on.clone(),
9646 (Some(on), Some(w)) => Some(Expr::Binary {
9647 lhs: Box::new(on),
9648 op: crate::ast::BinOp::And,
9649 rhs: Box::new(w),
9650 }),
9651 (Some(on), None) => Some(on),
9652 (None, w) => w,
9653 };
9654 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9655 // has no such clause on UPDATE, so this is accepted only under the
9656 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9657 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9658 let mut returning = self.parse_optional_returning()?;
9659 // v7.39 (round 533) — kept for the engine, which can resolve the
9660 // UNQUALIFIED leaves this lowering has to leave alone.
9661 let from_sources = from_clause.as_ref().map(|fc| {
9662 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9663 from: fc.clone(),
9664 sub_where: sub_where.clone(),
9665 })
9666 });
9667 let (assignments, where_) = if let Some(fc) = from_clause {
9668 let names: Vec<String> = core::iter::once(&fc.primary)
9669 .chain(fc.joins.iter().map(|j| &j.table))
9670 .flat_map(|t| {
9671 t.alias
9672 .clone()
9673 .into_iter()
9674 .chain(core::iter::once(t.name.clone()))
9675 })
9676 .collect();
9677 let refs_list = |e: &Expr| -> bool {
9678 fn walk(e: &Expr, names: &[String]) -> bool {
9679 match e {
9680 Expr::Column(c) => c
9681 .qualifier
9682 .as_deref()
9683 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9684 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9685 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9686 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9687 Expr::Case {
9688 operand,
9689 branches,
9690 else_branch,
9691 } => {
9692 operand.as_deref().is_some_and(|o| walk(o, names))
9693 || branches
9694 .iter()
9695 .any(|(w, t)| walk(w, names) || walk(t, names))
9696 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9697 }
9698 _ => false,
9699 }
9700 }
9701 walk(e, &names)
9702 };
9703 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9704 locking: None,
9705 ctes: Vec::new(),
9706 distinct: false,
9707 distinct_on: Vec::new(),
9708 items,
9709 from: Some(fc.clone()),
9710 where_: sub_where.clone(),
9711 group_by: None,
9712 group_by_all: false,
9713 having: None,
9714 unions: Vec::new(),
9715 order_by: Vec::new(),
9716 limit: None,
9717 offset: None,
9718 limit_with_ties: false,
9719 window_check_exprs: Vec::new(),
9720 };
9721 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9722 // assignment RHS with a correlated scalar subquery, instead of
9723 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9724 // column reference (`SET v = v + u.bonus`, where `v` is the target
9725 // table's column) inside a subquery whose FROM only has the source
9726 // table, so the unqualified `v` resolved against the source and
9727 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9728 // context — where they belong — fixes it; only the source columns
9729 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9730 // compound variants the leaf-walk doesn't decompose.
9731 let make_subq = |inner: Expr| {
9732 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9733 expr: inner,
9734 alias: None,
9735 }])))
9736 };
9737 let assignments = assignments
9738 .into_iter()
9739 .map(|(col, mut expr)| {
9740 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9741 (col, expr)
9742 })
9743 .collect();
9744 let exists = Expr::Exists {
9745 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9746 expr: Expr::Literal(Literal::Integer(1)),
9747 alias: None,
9748 }])),
9749 negated: false,
9750 };
9751 // v7.39 (round 241) — RETURNING may reference the FROM-list
9752 // tables too (`RETURNING emp.id, dept.name`); the same
9753 // leaf-to-correlated-subquery lowering the assignments get.
9754 // Without it the qualifier died at eval with "unknown table
9755 // qualifier". (RETURNING was parsed before this block — the
9756 // lowering is a pure AST transformation.)
9757 if let Some(items) = returning.as_mut() {
9758 for item in items.iter_mut() {
9759 if let SelectItem::Expr { expr, .. } = item {
9760 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9761 }
9762 }
9763 }
9764 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9765 // EVERY matching target row: it gets no EXISTS filter, but the
9766 // caller's WHERE still applies, with source columns read through
9767 // the correlated subquery (NULL when unmatched — LEFT-join
9768 // semantics). `sub_where` above already excluded the WHERE from
9769 // the source subquery for this case.
9770 if mysql_outer {
9771 let mut outer = where_;
9772 if let Some(w) = outer.as_mut() {
9773 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9774 }
9775 (assignments, outer)
9776 } else {
9777 (assignments, Some(exists))
9778 }
9779 } else {
9780 (assignments, where_)
9781 };
9782 Ok(Statement::Update(crate::ast::UpdateStatement {
9783 ctes: Vec::new(),
9784 table,
9785 only,
9786 alias,
9787 assignments,
9788 from_sources,
9789 where_,
9790 order_limit: update_order_limit,
9791 returning,
9792 }))
9793 }
9794
9795 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9796 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9797 /// clause and its meaning are identical, so both call this rather than
9798 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9799 /// legal. PG has no such clause on either statement, so it is read only
9800 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9801 /// errors.
9802 ///
9803 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9804 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9805 /// stack in round 430.
9806 #[inline(never)]
9807 fn parse_mysql_dml_order_limit(
9808 &mut self,
9809 what: &str,
9810 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9811 if !self.mysql_dialect {
9812 return Ok(None);
9813 }
9814 let order_by = self.parse_order_by_keys()?;
9815 let limit = if matches!(self.peek(), Token::Limit) {
9816 self.advance();
9817 let tok = self.advance();
9818 let Token::Integer(n) = tok else {
9819 return Err(self.err(alloc::format!(
9820 "expected integer after {what} LIMIT, got {tok:?}"
9821 )));
9822 };
9823 // MySQL rejects the `LIMIT offset, count` form here — only a
9824 // single row count is legal on a DML statement.
9825 if matches!(self.peek(), Token::Comma) {
9826 return Err(self.err(alloc::format!(
9827 "{what} LIMIT takes a row count, not an offset"
9828 )));
9829 }
9830 let n = u32::try_from(n)
9831 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9832 Some(n)
9833 } else {
9834 None
9835 };
9836 if order_by.is_empty() && limit.is_none() {
9837 return Ok(None);
9838 }
9839 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9840 order_by,
9841 limit,
9842 })))
9843 }
9844
9845 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9846 /// the leading `DELETE` ident.
9847 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9848 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9849 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9850 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9851 // parse here; it reaches the existing USING path with the target
9852 // repeated in the list, which the source-list peel below handles.)
9853 // More than one name is a multi-TARGET delete, which SPG does not
9854 // model; it is refused rather than half-applied.
9855 let mysql_pre_target: Option<String> =
9856 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9857 let first = self.expect_ident_like()?;
9858 if matches!(self.peek(), Token::Comma) {
9859 return Err(self.err(alloc::format!(
9860 "multi-table DELETE can only delete from one table; \
9861 `DELETE {first}, …` names several"
9862 )));
9863 }
9864 Some(first)
9865 } else {
9866 None
9867 };
9868 if !matches!(self.peek(), Token::From) {
9869 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9870 }
9871 self.advance();
9872 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9873 // lookahead as the UPDATE spelling.
9874 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9875 if s.eq_ignore_ascii_case("only"))
9876 && matches!(
9877 self.tokens.get(self.pos + 1),
9878 Some(Token::Ident(_) | Token::QuotedIdent(_))
9879 );
9880 if only {
9881 self.advance();
9882 }
9883 let table = self.expect_ident_like()?;
9884 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9885 // spelling must not swallow the clause keywords that can follow
9886 // the target.
9887 let alias = if matches!(self.peek(), Token::As) {
9888 self.advance();
9889 Some(self.expect_ident_like()?)
9890 } else {
9891 match self.peek() {
9892 Token::Ident(s) | Token::QuotedIdent(s)
9893 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9894 {
9895 let a = s.clone();
9896 self.advance();
9897 Some(a)
9898 }
9899 _ => None,
9900 }
9901 };
9902 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9903 // through the SAME join grammar the FROM clause uses (see the
9904 // `advance()`-destroys-tokens note on `parse_from_joins`).
9905 let mut mysql_on: Option<Expr> = None;
9906 let mut mysql_outer = false;
9907 let mysql_using = if mysql_pre_target.is_some()
9908 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9909 {
9910 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9911 let mut joins = self.parse_from_joins(&target_qual)?;
9912 if joins.is_empty() {
9913 return Err(self.err(alloc::string::String::from(
9914 "multi-table DELETE needs at least one source table",
9915 )));
9916 }
9917 let head = joins.remove(0);
9918 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9919 mysql_on = head.on;
9920 Some(FromClause {
9921 primary: head.table,
9922 joins,
9923 })
9924 } else {
9925 None
9926 };
9927 // The pre-FROM target must be the table the FROM names (or its
9928 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9929 // is not the scan target.
9930 if let Some(t) = &mysql_pre_target {
9931 let names_target = t.eq_ignore_ascii_case(&table)
9932 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9933 if !names_target {
9934 return Err(self.err(alloc::format!(
9935 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9936 )));
9937 }
9938 }
9939 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9940 // delete. Same lowering as UPDATE … FROM: the WHERE
9941 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9942 // target row by the correlated machinery.
9943 let using_clause = if let Some(fc) = mysql_using {
9944 Some(fc)
9945 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9946 self.advance();
9947 let mut fc = self.parse_from_clause()?;
9948 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9949 // repeats the TARGET as the first USING entry (PG's spelling
9950 // lists only the extra sources). Peel it so the source subquery
9951 // does not re-scan — and shadow — the target table.
9952 let primary_is_target =
9953 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9954 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9955 let head = fc.joins.remove(0);
9956 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9957 mysql_on = head.on;
9958 fc = FromClause {
9959 primary: head.table,
9960 joins: fc.joins,
9961 };
9962 }
9963 Some(fc)
9964 } else {
9965 None
9966 };
9967 let where_ = if matches!(self.peek(), Token::Where) {
9968 self.advance();
9969 Some(self.parse_expr(0)?)
9970 } else {
9971 None
9972 };
9973 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9974 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9975 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9976 let mut returning = self.parse_optional_returning()?;
9977 let where_ = if let Some(fc) = using_clause {
9978 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9979 // a USING-table reference in RETURNING becomes a correlated
9980 // scalar subquery over the USING list.
9981 let names: Vec<String> = core::iter::once(&fc.primary)
9982 .chain(fc.joins.iter().map(|j| &j.table))
9983 .flat_map(|t| {
9984 t.alias
9985 .clone()
9986 .into_iter()
9987 .chain(core::iter::once(t.name.clone()))
9988 })
9989 .collect();
9990 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9991 // join filters the SOURCE subquery on the ON predicate alone and
9992 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9993 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9994 // rows); every other form folds ON and WHERE into one EXISTS.
9995 let sub_where = match (mysql_on.clone(), where_.clone()) {
9996 _ if mysql_outer => mysql_on.clone(),
9997 (Some(on), Some(w)) => Some(Expr::Binary {
9998 lhs: Box::new(on),
9999 op: crate::ast::BinOp::And,
10000 rhs: Box::new(w),
10001 }),
10002 (Some(on), None) => Some(on),
10003 (None, w) => w,
10004 };
10005 let exists_where = sub_where.clone();
10006 let sub_fc = fc.clone();
10007 let make_subq = move |leaf: Expr| -> Expr {
10008 Expr::ScalarSubquery(Box::new(SelectStatement {
10009 locking: None,
10010 ctes: Vec::new(),
10011 distinct: false,
10012 distinct_on: Vec::new(),
10013 items: alloc::vec![SelectItem::Expr {
10014 expr: leaf,
10015 alias: None,
10016 }],
10017 from: Some(sub_fc.clone()),
10018 where_: sub_where.clone(),
10019 group_by: None,
10020 group_by_all: false,
10021 having: None,
10022 unions: Vec::new(),
10023 order_by: Vec::new(),
10024 limit: None,
10025 offset: None,
10026 limit_with_ties: false,
10027 window_check_exprs: Vec::new(),
10028 }))
10029 };
10030 let refs = |e: &Expr| expr_refs_tables(e, &names);
10031 if let Some(items) = returning.as_mut() {
10032 for item in items.iter_mut() {
10033 if let SelectItem::Expr { expr, .. } = item {
10034 wrap_from_leaves(expr, &names, &make_subq, &refs);
10035 }
10036 }
10037 }
10038 // A LEFT join deletes the target rows the WHERE selects, reading
10039 // source columns through the correlated subquery (NULL when
10040 // unmatched); no EXISTS row filter.
10041 if mysql_outer {
10042 let mut outer = where_;
10043 if let Some(w) = outer.as_mut() {
10044 wrap_from_leaves(w, &names, &make_subq, &refs);
10045 }
10046 outer
10047 } else {
10048 Some(Expr::Exists {
10049 subquery: Box::new(SelectStatement {
10050 locking: None,
10051 ctes: Vec::new(),
10052 distinct: false,
10053 distinct_on: Vec::new(),
10054 items: alloc::vec![SelectItem::Expr {
10055 expr: Expr::Literal(Literal::Integer(1)),
10056 alias: None,
10057 }],
10058 from: Some(fc),
10059 where_: exists_where,
10060 group_by: None,
10061 group_by_all: false,
10062 having: None,
10063 unions: Vec::new(),
10064 order_by: Vec::new(),
10065 limit: None,
10066 offset: None,
10067 limit_with_ties: false,
10068 window_check_exprs: Vec::new(),
10069 }),
10070 negated: false,
10071 })
10072 }
10073 } else {
10074 where_
10075 };
10076 Ok(Statement::Delete(crate::ast::DeleteStatement {
10077 ctes: Vec::new(),
10078 table,
10079 only,
10080 alias,
10081 where_,
10082 order_limit: delete_order_limit,
10083 returning,
10084 }))
10085 }
10086
10087 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10088 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10089 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10090 /// keyword. v7.17 surface:
10091 /// * source: table reference (subquery source is a follow-up)
10092 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10093 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10094 /// * AND-conditioned WHEN clauses; clauses tried in declaration
10095 /// order
10096 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10097 // INTO
10098 let is_into_kw = matches!(self.peek(), Token::Into)
10099 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10100 if !is_into_kw {
10101 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10102 }
10103 self.advance();
10104 let target = self.expect_ident_like()?;
10105 // Optional alias — bare ident before USING.
10106 let target_alias = match self.peek() {
10107 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10108 Some(self.expect_ident_like()?)
10109 }
10110 _ => None,
10111 };
10112 // USING
10113 let is_using_kw = matches!(
10114 self.peek(),
10115 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10116 );
10117 if !is_using_kw {
10118 return Err(self.err(format!(
10119 "expected USING after MERGE INTO target, got {:?}",
10120 self.peek()
10121 )));
10122 }
10123 self.advance();
10124 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10125 // <table> [alias]`. PG requires an alias after a subquery source.
10126 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10127 self.advance(); // (
10128 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10129 // constant-SELECT lowering the derived-table parser uses
10130 // (PG deletes through this form; it was a parse error).
10131 let inner = if matches!(self.peek(), Token::Values) {
10132 self.advance(); // VALUES
10133 Statement::Select(self.parse_values_rows_body()?)
10134 } else {
10135 self.parse_select_stmt()?
10136 };
10137 match self.advance() {
10138 Token::RParen => {}
10139 other => {
10140 return Err(self.err(format!(
10141 "expected ')' after MERGE USING subquery, got {other:?}"
10142 )));
10143 }
10144 }
10145 let Statement::Select(sub) = inner else {
10146 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10147 };
10148 (String::new(), Some(Box::new(sub)))
10149 } else {
10150 (self.expect_ident_like()?, None)
10151 };
10152 let source_alias = match self.peek() {
10153 Token::Ident(s) | Token::QuotedIdent(s)
10154 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10155 {
10156 Some(self.expect_ident_like()?)
10157 }
10158 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10159 self.advance(); // AS
10160 Some(self.expect_ident_like()?)
10161 }
10162 _ => None,
10163 };
10164 // v7.39 (round 768, F31-D5) — optional positional column-alias
10165 // list after the source alias (`s(id, v)`).
10166 let mut source_column_aliases: Vec<String> = Vec::new();
10167 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10168 self.advance();
10169 loop {
10170 source_column_aliases.push(self.expect_ident_like()?);
10171 match self.peek() {
10172 Token::Comma => {
10173 self.advance();
10174 }
10175 Token::RParen => {
10176 self.advance();
10177 break;
10178 }
10179 other => {
10180 return Err(self.err(format!(
10181 "expected ',' or ')' in MERGE source column list, got {other:?}"
10182 )));
10183 }
10184 }
10185 }
10186 }
10187 if source_select.is_some() && source_alias.is_none() {
10188 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10189 }
10190 // ON
10191 if !matches!(self.peek(), Token::On) {
10192 return Err(self.err(format!(
10193 "expected ON after MERGE … USING source, got {:?}",
10194 self.peek()
10195 )));
10196 }
10197 self.advance();
10198 let on = self.parse_expr(0)?;
10199 // One or more WHEN clauses.
10200 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10201 loop {
10202 let is_when_kw = matches!(
10203 self.peek(),
10204 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10205 );
10206 if !is_when_kw {
10207 break;
10208 }
10209 self.advance(); // WHEN
10210 // [NOT] MATCHED
10211 let matched = if matches!(self.peek(), Token::Not) {
10212 self.advance();
10213 crate::ast::MergeMatched::NotMatched
10214 } else {
10215 crate::ast::MergeMatched::Matched
10216 };
10217 let is_matched_kw = matches!(
10218 self.peek(),
10219 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10220 );
10221 if !is_matched_kw {
10222 return Err(self.err(format!(
10223 "expected MATCHED in WHEN clause, got {:?}",
10224 self.peek()
10225 )));
10226 }
10227 self.advance();
10228 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10229 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10230 // to fire for target rows no source row matches.
10231 let mut matched = matched;
10232 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10233 self.advance();
10234 match self.peek() {
10235 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10236 self.advance();
10237 matched = crate::ast::MergeMatched::NotMatchedBySource;
10238 }
10239 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10240 self.advance();
10241 }
10242 other => {
10243 return Err(self.err(format!(
10244 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10245 )));
10246 }
10247 }
10248 }
10249 // Optional AND <expr>
10250 let condition = if matches!(self.peek(), Token::And) {
10251 self.advance();
10252 Some(self.parse_expr(0)?)
10253 } else {
10254 None
10255 };
10256 // THEN
10257 let is_then_kw = matches!(
10258 self.peek(),
10259 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10260 );
10261 if !is_then_kw {
10262 return Err(self.err(format!(
10263 "expected THEN in WHEN clause, got {:?}",
10264 self.peek()
10265 )));
10266 }
10267 self.advance();
10268 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10269 let action = match self.peek().clone() {
10270 Token::Insert => {
10271 self.advance();
10272 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10273 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10274 // VALUES (…)` omits it and fills every column in declaration
10275 // order. PG accepts this; SPG used to require the list.
10276 let mut columns: Vec<String> = Vec::new();
10277 if matches!(self.peek(), Token::LParen) {
10278 self.advance();
10279 loop {
10280 columns.push(self.expect_ident_like()?);
10281 if matches!(self.peek(), Token::Comma) {
10282 self.advance();
10283 continue;
10284 }
10285 break;
10286 }
10287 if !matches!(self.peek(), Token::RParen) {
10288 return Err(self.err(format!(
10289 "expected ')' after INSERT column list, got {:?}",
10290 self.peek()
10291 )));
10292 }
10293 self.advance();
10294 }
10295 // VALUES (...)
10296 if !matches!(self.peek(), Token::Values) {
10297 return Err(self.err(format!(
10298 "expected VALUES in MERGE INSERT, got {:?}",
10299 self.peek()
10300 )));
10301 }
10302 self.advance();
10303 if !matches!(self.peek(), Token::LParen) {
10304 return Err(self.err(format!(
10305 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10306 self.peek()
10307 )));
10308 }
10309 self.advance();
10310 let mut values: Vec<crate::ast::Expr> = Vec::new();
10311 loop {
10312 values.push(self.parse_expr(0)?);
10313 if matches!(self.peek(), Token::Comma) {
10314 self.advance();
10315 continue;
10316 }
10317 break;
10318 }
10319 if !matches!(self.peek(), Token::RParen) {
10320 return Err(self.err(format!(
10321 "expected ')' after MERGE INSERT values, got {:?}",
10322 self.peek()
10323 )));
10324 }
10325 self.advance();
10326 // Empty column list = positional into every column, so the
10327 // count is checked against the table arity at execution.
10328 if !columns.is_empty() && columns.len() != values.len() {
10329 return Err(self.err(format!(
10330 "MERGE INSERT column count ({}) ≠ value count ({})",
10331 columns.len(),
10332 values.len()
10333 )));
10334 }
10335 crate::ast::MergeAction::Insert { columns, values }
10336 }
10337 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10338 self.advance();
10339 // SET
10340 let is_set_kw = matches!(
10341 self.peek(),
10342 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10343 );
10344 if !is_set_kw {
10345 return Err(self.err(format!(
10346 "expected SET after UPDATE in MERGE, got {:?}",
10347 self.peek()
10348 )));
10349 }
10350 self.advance();
10351 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10352 loop {
10353 let col = self.expect_ident_like()?;
10354 if !matches!(self.peek(), Token::Eq) {
10355 return Err(self.err(format!(
10356 "expected '=' in MERGE UPDATE assignment, got {:?}",
10357 self.peek()
10358 )));
10359 }
10360 self.advance();
10361 let expr = self.parse_expr(0)?;
10362 assignments.push((col, expr));
10363 if matches!(self.peek(), Token::Comma) {
10364 self.advance();
10365 continue;
10366 }
10367 break;
10368 }
10369 crate::ast::MergeAction::Update { assignments }
10370 }
10371 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10372 self.advance();
10373 crate::ast::MergeAction::Delete
10374 }
10375 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10376 self.advance();
10377 let is_nothing_kw = matches!(
10378 self.peek(),
10379 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10380 );
10381 if !is_nothing_kw {
10382 return Err(self.err(format!(
10383 "expected NOTHING after DO in MERGE clause, got {:?}",
10384 self.peek()
10385 )));
10386 }
10387 self.advance();
10388 crate::ast::MergeAction::DoNothing
10389 }
10390 other => {
10391 return Err(self.err(format!(
10392 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10393 )));
10394 }
10395 };
10396 // PG's grammar simply has no INSERT production under BY SOURCE
10397 // (a target row already exists there) — same syntax error.
10398 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10399 && matches!(action, crate::ast::MergeAction::Insert { .. })
10400 {
10401 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10402 }
10403 clauses.push(crate::ast::MergeWhenClause {
10404 matched,
10405 condition,
10406 action,
10407 });
10408 }
10409 if clauses.is_empty() {
10410 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10411 }
10412 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10413 // unconditional (no `AND`) WHEN of the same match kind: it could
10414 // never fire. Check per match kind in clause order.
10415 let mut seen_unconditional_matched = false;
10416 let mut seen_unconditional_not_matched = false;
10417 let mut seen_unconditional_by_source = false;
10418 for c in &clauses {
10419 let seen = match c.matched {
10420 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10421 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10422 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10423 };
10424 if *seen {
10425 return Err(self.err(String::from(
10426 "unreachable WHEN clause specified after unconditional WHEN clause",
10427 )));
10428 }
10429 if c.condition.is_none() {
10430 *seen = true;
10431 }
10432 }
10433 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10434 let returning = self.parse_optional_returning()?;
10435 Ok(Statement::Merge(crate::ast::MergeStatement {
10436 // Attached by `parse_with_cte_then_select` when the MERGE
10437 // heads a WITH clause (round 149).
10438 ctes: Vec::new(),
10439 target,
10440 target_alias,
10441 source,
10442 source_alias,
10443 source_select,
10444 source_column_aliases,
10445 on,
10446 clauses,
10447 returning,
10448 }))
10449 }
10450
10451 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10452 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10453 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10454 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10455 fn parse_optional_returning(
10456 &mut self,
10457 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10458 let is_returning_kw = matches!(
10459 self.peek(),
10460 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10461 );
10462 if !is_returning_kw {
10463 return Ok(None);
10464 }
10465 self.advance();
10466 let mut items = Vec::new();
10467 loop {
10468 items.push(self.parse_select_item()?);
10469 if matches!(self.peek(), Token::Comma) {
10470 self.advance();
10471 continue;
10472 }
10473 break;
10474 }
10475 Ok(Some(items))
10476 }
10477
10478 /// v6.0.4 — parse the tail of an ALTER statement after the
10479 /// leading `ALTER` keyword has been consumed. Only one form is
10480 /// supported in v6.0.4:
10481 ///
10482 /// ```text
10483 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10484 /// ```
10485 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10486 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10487 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10488 // exclusion) is accepted by stripping the `ONLY` keyword
10489 // before the table parse.
10490 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10491 // and the long PG-dump tail are accepted as no-ops.
10492 match self.advance() {
10493 Token::Index => {}
10494 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10495 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10496 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10497 Token::Table => {
10498 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10499 self.advance();
10500 }
10501 return self.parse_alter_table_after_keyword();
10502 }
10503 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10504 return self.parse_alter_policy_after_keyword();
10505 }
10506 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10507 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10508 self.advance();
10509 }
10510 return self.parse_alter_table_after_keyword();
10511 }
10512 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10513 // of the silent-noop tail.
10514 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10515 return self.parse_alter_sequence_after_keyword();
10516 }
10517 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10518 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10519 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10520 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10521 // NB: the match arm consumed `TYPE` via self.advance(); the
10522 // cursor is now at the type name — do NOT advance again.
10523 let type_name = self.expect_ident_like()?;
10524 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10525 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10526 if is_add_value {
10527 self.advance(); // ADD
10528 self.advance(); // VALUE
10529 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10530 // IF/EXISTS as identifiers.
10531 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10532 {
10533 let n1 = self.tokens.get(self.pos + 1);
10534 let n2 = self.tokens.get(self.pos + 2);
10535 if matches!(n1, Some(Token::Not))
10536 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10537 {
10538 self.advance();
10539 self.advance();
10540 self.advance();
10541 true
10542 } else {
10543 false
10544 }
10545 } else {
10546 false
10547 };
10548 let label = self.expect_string_literal()?;
10549 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10550 {
10551 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10552 self.advance();
10553 let anchor = self.expect_string_literal()?;
10554 Some((is_before, anchor))
10555 } else {
10556 None
10557 };
10558 return Ok(Statement::AlterTypeAddValue {
10559 type_name,
10560 label,
10561 if_not_exists,
10562 position,
10563 });
10564 }
10565 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10566 // Used to fall into the no-op tail below: accepted, silently
10567 // ignored. `RENAME TO <newtype>` keeps falling through.
10568 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10569 && matches!(
10570 self.tokens.get(self.pos + 1),
10571 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10572 )
10573 {
10574 self.advance(); // RENAME
10575 self.advance(); // VALUE
10576 let old = self.expect_string_literal()?;
10577 if matches!(self.peek(), Token::To) {
10578 self.advance();
10579 } else {
10580 self.expect_keyword_ident("to")?;
10581 }
10582 let new = self.expect_string_literal()?;
10583 return Ok(Statement::AlterTypeRenameValue {
10584 type_name,
10585 old,
10586 new,
10587 });
10588 }
10589 // Other ALTER TYPE forms — the ACTION stays a no-op
10590 // (pg_dump tail), but v7.39 (round 708) the NAME is
10591 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10592 // success for a type that does not exist.
10593 self.consume_until_statement_boundary();
10594 return Ok(Statement::ValidateOnly {
10595 kind: crate::ast::ValidateOnlyKind::TypeName,
10596 names: alloc::vec![type_name],
10597 });
10598 }
10599 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10600 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10601 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10602 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10603 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10604 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10605 // pg_dump no-op list below: every form used to report success
10606 // and change nothing, which is worse than refusing outright
10607 // (a migration dropping a constraint kept rejecting data).
10608 // NOTE: the enclosing `match self.advance()` already consumed
10609 // the DOMAIN keyword, so the name is next.
10610 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10611 return self.parse_alter_domain_after_keyword();
10612 }
10613 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10614 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10615 // used to fall into the pg_dump no-op tail below, so a DBA
10616 // setting a per-role default was told it worked and nothing
10617 // happened. Intercepted here, BEFORE that tail.
10618 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10619 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10620 // interception below exists: swallowed with the no-op tail, an
10621 // unknown parameter name was ACCEPTED where PG18 answers
10622 // `unrecognized configuration parameter`. SPG applies nothing
10623 // either way — there is no postgresql.auto.conf — but it now
10624 // says so about a name it does not know.
10625 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10626 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10627 // already consumed here. An extra advance eats the SET and
10628 // the parameter name is never seen — which is exactly the
10629 // bug a panic in this branch disproved: the branch WAS on
10630 // the path, the reading of it was wrong.
10631 let mut parameter = None;
10632 // SET <name> … | RESET <name> | RESET ALL
10633 if matches!(self.peek(), Token::Ident(k)
10634 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10635 {
10636 self.advance();
10637 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10638 && !n.eq_ignore_ascii_case("all")
10639 {
10640 self.advance();
10641 // A dotted GUC (`plpgsql.check_asserts`) is two
10642 // tokens; keep the whole name.
10643 let mut full = n;
10644 while matches!(self.peek(), Token::Dot) {
10645 self.advance();
10646 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10647 full.push('.');
10648 full.push_str(&t);
10649 }
10650 }
10651 parameter = Some(full);
10652 }
10653 }
10654 self.consume_until_statement_boundary();
10655 return Ok(Statement::AlterSystem { parameter });
10656 }
10657 Token::Ident(s) | Token::QuotedIdent(s)
10658 if matches!(
10659 s.to_ascii_lowercase().as_str(),
10660 "role" | "user" | "database"
10661 ) && self.peeks_db_role_setting() =>
10662 {
10663 let is_database = s.eq_ignore_ascii_case("database");
10664 return self.parse_db_role_setting(is_database);
10665 }
10666 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10667 // (the non-SET forms; SET/RESET took the branch above). The
10668 // attributes still no-op — recorded, and the ignored PASSWORD
10669 // is ledgered as its own follow-up — but the ROLE is validated:
10670 // any name was accepted for a role that does not exist.
10671 Token::Ident(s) | Token::QuotedIdent(s)
10672 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10673 {
10674 // NB: the enclosing `match self.advance()` already consumed
10675 // ROLE/USER — the round-695 trap, hit again in this round's
10676 // first draft (the name was eaten and WITH parsed as the
10677 // role). The cursor is at the name.
10678 let name = self.expect_ident_or_string()?;
10679 // v7.39 (round 750) — scan the attribute tail for
10680 // PASSWORD. Everything else stays a recorded no-op, but
10681 // a dropped credential rotation is a SECURITY bug:
10682 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10683 // changed nothing, so the old password kept working.
10684 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10685 // NULL` clears the credential.
10686 let mut password: Option<Option<String>> = None;
10687 loop {
10688 match self.peek() {
10689 Token::Semicolon | Token::Eof => break,
10690 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10691 self.advance();
10692 match self.advance() {
10693 Token::String(p) => password = Some(Some(p)),
10694 Token::Null => password = Some(None),
10695 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10696 password = Some(None);
10697 }
10698 other => {
10699 return Err(self.err(alloc::format!(
10700 "expected password string or NULL after PASSWORD, got {other:?}"
10701 )));
10702 }
10703 }
10704 }
10705 _ => {
10706 self.advance();
10707 }
10708 }
10709 }
10710 if name.eq_ignore_ascii_case("all") {
10711 // `ALTER ROLE ALL …` names every role; nothing to check.
10712 return Ok(Statement::Empty);
10713 }
10714 if let Some(pw) = password {
10715 return Ok(Statement::AlterRolePassword { name, password: pw });
10716 }
10717 return Ok(Statement::ValidateOnly {
10718 kind: crate::ast::ValidateOnlyKind::RoleName,
10719 names: alloc::vec![name],
10720 });
10721 }
10722 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10723 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10724 // list far enough to validate the NAME; the actions still no-op.
10725 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10726 // models none of them and their dumps are rare.)
10727 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10728 let name = self.expect_ident_or_string()?;
10729 self.consume_until_statement_boundary();
10730 return Ok(Statement::ValidateOnly {
10731 kind: crate::ast::ValidateOnlyKind::CollationName,
10732 names: alloc::vec![name],
10733 });
10734 }
10735 Token::Ident(s) | Token::QuotedIdent(s)
10736 if s.eq_ignore_ascii_case("text")
10737 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10738 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10739 {
10740 self.advance(); // SEARCH
10741 self.advance(); // CONFIGURATION
10742 let name = self.expect_ident_like()?;
10743 self.consume_until_statement_boundary();
10744 return Ok(Statement::ValidateOnly {
10745 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10746 names: alloc::vec![name],
10747 });
10748 }
10749 Token::Ident(s) | Token::QuotedIdent(s)
10750 if s.eq_ignore_ascii_case("event")
10751 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10752 {
10753 self.advance(); // TRIGGER
10754 let name = self.expect_ident_like()?;
10755 self.consume_until_statement_boundary();
10756 return Ok(Statement::ValidateOnly {
10757 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10758 names: alloc::vec![name],
10759 });
10760 }
10761 Token::Ident(s) | Token::QuotedIdent(s)
10762 if s.eq_ignore_ascii_case("large")
10763 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10764 {
10765 self.advance(); // OBJECT
10766 let oid = match self.advance() {
10767 Token::Integer(n) => alloc::format!("{n}"),
10768 other => {
10769 return Err(
10770 self.err(alloc::format!("expected large object oid, got {other:?}"))
10771 );
10772 }
10773 };
10774 self.consume_until_statement_boundary();
10775 return Ok(Statement::ValidateOnly {
10776 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10777 names: alloc::vec![oid],
10778 });
10779 }
10780 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10781 // argument-list parse as DROP AGGREGATE (round 707); the
10782 // action no-ops, the existence check is real.
10783 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10784 // Same round-695 trap as above: AGGREGATE is already
10785 // consumed; the cursor is at the name.
10786 let name = self.expect_ident_like()?;
10787 let mut names = alloc::vec![name];
10788 if matches!(self.peek(), Token::LParen) {
10789 self.advance();
10790 loop {
10791 match self.peek().clone() {
10792 Token::RParen => {
10793 self.advance();
10794 break;
10795 }
10796 Token::Star => {
10797 self.advance();
10798 names.push(String::from("*"));
10799 }
10800 Token::Comma => {
10801 self.advance();
10802 }
10803 _ => {
10804 let mut t = self.expect_ident_like()?;
10805 while let Token::Ident(nx) = self.peek() {
10806 let nx = nx.clone();
10807 self.advance();
10808 t.push(' ');
10809 t.push_str(&nx);
10810 }
10811 names.push(t);
10812 }
10813 }
10814 }
10815 }
10816 self.consume_until_statement_boundary();
10817 return Ok(Statement::ValidateOnly {
10818 kind: crate::ast::ValidateOnlyKind::AggregateName,
10819 names,
10820 });
10821 }
10822 Token::Ident(s) | Token::QuotedIdent(s)
10823 if matches!(
10824 s.to_ascii_lowercase().as_str(),
10825 "view"
10826 | "function"
10827 | "database"
10828 | "schema"
10829 | "owner"
10830 | "default"
10831 | "extension"
10832 | "materialized"
10833 | "publication"
10834 | "subscription"
10835 // v7.37.17 (17.6 siblings) — additional ALTER
10836 // targets pg_dump / pg_dumpall / operator DB
10837 // migration scripts commonly emit. SPG has
10838 // no matching machinery for any of these; the
10839 // parser accepts + Empty-returns so pg_dump
10840 // tail statements don't stall.
10841 | "tablespace"
10842 | "language"
10843 | "operator"
10844 | "conversion"
10845 | "statistics"
10846 | "server"
10847 | "foreign"
10848 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10849 // / TEMPLATE (CONFIGURATION intercepted above).
10850 | "text"
10851 ) =>
10852 {
10853 self.consume_until_statement_boundary();
10854 return Ok(Statement::Empty);
10855 }
10856 other => {
10857 return Err(self.err(format!(
10858 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10859 after ALTER, got {other:?}"
10860 )));
10861 }
10862 }
10863 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10864 // (mailrs migrate-042 ships these). The presence of an
10865 // IF EXISTS makes the subsequent name lookup tolerate
10866 // a missing index — engine returns CommandOk no-op.
10867 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10868 let next = self.tokens.get(self.pos + 1);
10869 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10870 self.advance();
10871 self.advance();
10872 true
10873 } else {
10874 false
10875 }
10876 } else {
10877 false
10878 };
10879 let name = self.expect_ident_like()?;
10880 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10881 // Detect BEFORE the REBUILD path so the existing REBUILD
10882 // arm stays untouched.
10883 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10884 self.advance();
10885 if matches!(self.peek(), Token::To) {
10886 self.advance();
10887 } else {
10888 self.expect_keyword_ident("to")?;
10889 }
10890 let new = self.expect_ident_like()?;
10891 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10892 name,
10893 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10894 }));
10895 }
10896 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10897 // A syntax error before; the index is validated, the params no-op.
10898 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10899 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10900 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10901 {
10902 self.consume_until_statement_boundary();
10903 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10904 name,
10905 target: crate::ast::AlterIndexTarget::StorageParams,
10906 }));
10907 }
10908 // REBUILD
10909 self.expect_keyword_ident("rebuild")?;
10910 // Optional: WITH (encoding = <enc>)
10911 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10912 self.advance();
10913 if !matches!(self.peek(), Token::LParen) {
10914 return Err(self.err(format!(
10915 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10916 self.peek()
10917 )));
10918 }
10919 self.advance();
10920 self.expect_keyword_ident("encoding")?;
10921 if !matches!(self.peek(), Token::Eq) {
10922 return Err(self.err(format!(
10923 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10924 self.peek()
10925 )));
10926 }
10927 self.advance();
10928 let enc_ident = match self.advance() {
10929 Token::Ident(s) | Token::QuotedIdent(s) => s,
10930 other => {
10931 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10932 }
10933 };
10934 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10935 "f32" => VecEncoding::F32,
10936 "sq8" => VecEncoding::Sq8,
10937 "half" => VecEncoding::F16,
10938 other => {
10939 return Err(self.err(format!(
10940 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10941 )));
10942 }
10943 };
10944 if !matches!(self.peek(), Token::RParen) {
10945 return Err(self.err(format!(
10946 "expected ')' after encoding value, got {:?}",
10947 self.peek()
10948 )));
10949 }
10950 self.advance();
10951 Some(enc)
10952 } else {
10953 None
10954 };
10955 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10956 name,
10957 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10958 }))
10959 }
10960
10961 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10962 /// only `SET` form currently supported; future v6.7.x can add
10963 /// more SET subjects without changing the dispatch shape.
10964 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10965 /// subactions. Single-subaction shape stays a 1-element vec.
10966 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10967 let table_name = self.expect_ident_like()?;
10968 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10969 loop {
10970 let subaction = self.parse_alter_table_subaction()?;
10971 // ADD COLUMN with inline REFERENCES emits both an
10972 // AddColumn and an AddForeignKey subaction; the
10973 // helper returns 1 or 2 items.
10974 targets.extend(subaction);
10975 if matches!(self.peek(), Token::Comma) {
10976 self.advance();
10977 continue;
10978 }
10979 break;
10980 }
10981 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10982 name: table_name,
10983 targets,
10984 }))
10985 }
10986
10987 /// Parse one ALTER TABLE subaction. Returns a Vec because
10988 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10989 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10990 /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
10991 /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
10992 /// appends.
10993 fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
10994 match self.peek() {
10995 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
10996 self.advance();
10997 Some(crate::ast::ColumnPosition::First)
10998 }
10999 Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
11000 self.advance();
11001 let name = self.expect_ident_like().ok()?;
11002 Some(crate::ast::ColumnPosition::After(name))
11003 }
11004 _ => None,
11005 }
11006 }
11007
11008 fn parse_alter_table_subaction(
11009 &mut self,
11010 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11011 match self.peek() {
11012 // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
11013 // a statement a real migration emits and SPG answered 1064
11014 // for; measured against MySQL 9.7.2, one at a time, beside
11015 // the published image.
11016 Token::Ident(s)
11017 if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
11018 {
11019 let changing = s.eq_ignore_ascii_case("change");
11020 self.advance();
11021 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
11022 self.advance();
11023 }
11024 // `parse_column_def_with_fk` reads the NAME itself, so
11025 // `MODIFY` hands it the column and `CHANGE` eats the old
11026 // name first and lets it read the new one.
11027 let old_name = if changing {
11028 Some(self.expect_ident_like()?)
11029 } else {
11030 None
11031 };
11032 let (definition, _fk) = self.parse_column_def_with_fk()?;
11033 let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11034 let rename_to = if changing {
11035 Some(definition.name.clone())
11036 } else {
11037 None
11038 };
11039 let position = self.parse_column_position();
11040 Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11041 column,
11042 rename_to,
11043 definition,
11044 position,
11045 }])
11046 }
11047 Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11048 self.advance();
11049 if matches!(self.peek(), Token::Eq) {
11050 self.advance();
11051 }
11052 let n = self.expect_u64_literal()?;
11053 Ok(alloc::vec![
11054 crate::ast::AlterTableTarget::SetTableAutoIncrement(
11055 i64::try_from(n).unwrap_or(i64::MAX)
11056 )
11057 ])
11058 }
11059 Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11060 self.advance();
11061 if matches!(self.peek(), Token::Eq) {
11062 self.advance();
11063 }
11064 // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11065 // clause has kept it since v7.39.3. The lexer folds a
11066 // bare identifier, and MySQL names the engine back
11067 // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11068 // answers `Unknown storage engine 'NoSuchEng'` there and
11069 // answered `'nosucheng'` here — the one thing that
11070 // message is for is telling the operator which word in
11071 // their migration was wrong.
11072 let at = self.pos;
11073 let name = self.expect_ident_like()?;
11074 let written = self
11075 .source_span(at, at)
11076 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11077 .filter(|raw| raw.eq_ignore_ascii_case(&name))
11078 .map(alloc::string::String::from);
11079 Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11080 written.unwrap_or(name)
11081 )])
11082 }
11083 Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11084 self.advance();
11085 // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11086 if matches!(self.peek(), Token::To) {
11087 self.advance();
11088 }
11089 let kw = self.expect_ident_like()?;
11090 if !kw.eq_ignore_ascii_case("character") {
11091 return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11092 }
11093 let set_kw = self.expect_ident_like()?;
11094 if !set_kw.eq_ignore_ascii_case("set") {
11095 return Err(self.err("expected SET after CHARACTER".into()));
11096 }
11097 let charset = self.expect_ident_like()?;
11098 let collate =
11099 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11100 self.advance();
11101 Some(self.expect_ident_like()?)
11102 } else {
11103 None
11104 };
11105 Ok(alloc::vec![
11106 crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11107 ])
11108 }
11109 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11110 self.advance();
11111 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11112 // storage parameters: paren-prefixed; consume.
11113 if matches!(self.peek(), Token::LParen) {
11114 self.consume_until_statement_boundary();
11115 return Ok(Vec::new());
11116 }
11117 let setting = self.expect_ident_like()?;
11118 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11119 if !matches!(self.peek(), Token::Eq) {
11120 return Err(self.err(alloc::format!(
11121 "expected '=' after hot_tier_bytes, got {:?}",
11122 self.peek()
11123 )));
11124 }
11125 self.advance();
11126 let n = self.expect_u64_literal()?;
11127 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11128 }
11129 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11130 // accept-and-no-op for ALTER TABLE SET <subject>
11131 // forms that pg_dump emits but SPG either treats
11132 // as N/A (single-tenant, single-owner, no shared
11133 // tablespaces) or accepts the dump-side declaration
11134 // without runtime effect:
11135 // SET SCHEMA <name> (18.11)
11136 // SET TABLESPACE <name> (18.8)
11137 // SET LOGGED / UNLOGGED (18.7 alt-form)
11138 // SET WITHOUT CLUSTER (18.13)
11139 // SET WITHOUT OIDS (PG legacy)
11140 // SET (option = value, …) (storage parameters)
11141 // SET REPLICA IDENTITY {…} (18.14)
11142 if setting.eq_ignore_ascii_case("schema")
11143 || setting.eq_ignore_ascii_case("tablespace")
11144 || setting.eq_ignore_ascii_case("logged")
11145 || setting.eq_ignore_ascii_case("unlogged")
11146 || setting.eq_ignore_ascii_case("without")
11147 {
11148 self.consume_until_statement_boundary();
11149 return Ok(Vec::new());
11150 }
11151 if setting.eq_ignore_ascii_case("replica") {
11152 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11153 self.consume_until_statement_boundary();
11154 return Ok(Vec::new());
11155 }
11156 // SET (option=value, …) — storage parameters.
11157 if matches!(self.peek(), Token::LParen) {
11158 self.consume_until_statement_boundary();
11159 return Ok(Vec::new());
11160 }
11161 Err(self.err(alloc::format!(
11162 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11163 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11164 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11165 )))
11166 }
11167 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11168 // not ignored: round 645 gave SPG the inheritance the
11169 // v7.37.18 no-op said it did not have.
11170 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11171 self.advance();
11172 let parent = self.expect_ident_like()?;
11173 self.consume_until_statement_boundary();
11174 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11175 parent,
11176 detach: false
11177 }])
11178 }
11179 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11180 // LEVEL SECURITY`, which has its own RLS arm below — without
11181 // the guard this swallowed NO FORCE as a no-op.
11182 Token::Ident(s)
11183 if s.eq_ignore_ascii_case("no")
11184 && !matches!(
11185 self.tokens.get(self.pos + 1),
11186 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11187 ) =>
11188 {
11189 self.advance();
11190 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11191 if k.eq_ignore_ascii_case("inherit"))
11192 {
11193 self.advance();
11194 let parent = self.expect_ident_like()?;
11195 self.consume_until_statement_boundary();
11196 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11197 parent,
11198 detach: true
11199 }]);
11200 }
11201 self.consume_until_statement_boundary();
11202 Ok(Vec::new())
11203 }
11204 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11205 // single-owner, so there is still nothing to record.
11206 //
11207 // v7.39 (round 652) — but the name now reaches the engine,
11208 // which refuses a role that does not exist as PG does. The
11209 // no-op was swallowing the whole statement, so a dump naming
11210 // a role this server never heard of restored clean and left
11211 // the table owned by whoever ran the restore.
11212 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11213 self.advance();
11214 if matches!(self.peek(), Token::To) {
11215 self.advance();
11216 }
11217 let role = self.expect_ident_like()?;
11218 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11219 role
11220 }])
11221 }
11222 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11223 // PG sets a hint; SPG doesn't have clustered storage, so the
11224 // hint itself stays a no-op.
11225 //
11226 // v7.39 (round 652) — the index name is checked now. PG
11227 // errors on one that does not exist, and swallowing that let
11228 // a typo'd CLUSTER ON pass silently.
11229 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11230 self.advance();
11231 // `ON` is a reserved token, not an ident.
11232 if !matches!(self.peek(), Token::On) {
11233 return Err(self.err(alloc::format!(
11234 "expected ON after CLUSTER, got {:?}",
11235 self.peek()
11236 )));
11237 }
11238 self.advance();
11239 let index = self.expect_ident_like()?;
11240 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11241 index: Some(index)
11242 }])
11243 }
11244 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11245 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11246 // what a logical decoder puts in the old-tuple image; SPG's
11247 // replication is SQL-text, so there is nothing to record.
11248 // Accept-and-no-op (it used to be a parse error).
11249 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11250 self.advance();
11251 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11252 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11253 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11254 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11255 {
11256 self.advance(); // IDENTITY
11257 self.advance(); // USING
11258 if matches!(self.peek(), Token::Index)
11259 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11260 {
11261 self.advance();
11262 }
11263 let index = self.expect_ident_like()?;
11264 self.consume_until_statement_boundary();
11265 return Ok(alloc::vec![
11266 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11267 ]);
11268 }
11269 self.consume_until_statement_boundary();
11270 Ok(Vec::new())
11271 }
11272 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11273 //
11274 // v7.39 (round 652) — it used to consume the statement and
11275 // return nothing, on the stated theory that SPG validated at
11276 // ADD CONSTRAINT time so there was never anything left to
11277 // validate. Measured against PG18, ADD CONSTRAINT did not
11278 // scan the existing rows at all — the comment described a
11279 // property SPG did not have, which is why nobody looked. Both
11280 // halves are real now: ADD scans unless told NOT VALID, and
11281 // this scans what NOT VALID skipped.
11282 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11283 self.advance();
11284 self.expect_keyword_ident("constraint")?;
11285 let name = self.expect_ident_like()?;
11286 Ok(alloc::vec![
11287 crate::ast::AlterTableTarget::ValidateConstraint { name }
11288 ])
11289 }
11290 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11291 // SET (option = value, …). PG uses it to clear per-table
11292 // storage params like fillfactor or autovacuum_*. SPG
11293 // engine-manages those parameters; accept-and-no-op.
11294 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11295 self.advance();
11296 self.consume_until_statement_boundary();
11297 Ok(Vec::new())
11298 }
11299 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11300 // type-of binding (PG 9.0+). SPG composite types
11301 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11302 // TABLE OF is rare and inverse of CREATE TABLE OF.
11303 // Accept-and-no-op until a customer dump round-trips it.
11304 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11305 self.advance();
11306 // v7.39 (round 710) — the type name is validated now.
11307 let type_name = self.expect_ident_like()?;
11308 self.consume_until_statement_boundary();
11309 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11310 type_name
11311 }])
11312 }
11313 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11314 // (reserved keyword) rather than Token::Ident("not"),
11315 // so it needs its own arm. Accept-and-no-op same as OF.
11316 Token::Not => {
11317 self.advance();
11318 self.consume_until_statement_boundary();
11319 Ok(Vec::new())
11320 }
11321 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11322 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11323 self.advance();
11324 self.expect_row_level_security()?;
11325 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11326 enabled: None,
11327 force: Some(true),
11328 }])
11329 }
11330 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11331 Token::Ident(s)
11332 if s.eq_ignore_ascii_case("no")
11333 && matches!(
11334 self.tokens.get(self.pos + 1),
11335 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11336 ) =>
11337 {
11338 self.advance(); // NO
11339 self.advance(); // FORCE
11340 self.expect_row_level_security()?;
11341 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11342 enabled: None,
11343 force: Some(false),
11344 }])
11345 }
11346 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11347 // (sets relrowsecurity). The guard requires the next token to be
11348 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11349 Token::Ident(s)
11350 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11351 && matches!(
11352 self.tokens.get(self.pos + 1),
11353 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11354 ) =>
11355 {
11356 let enabled = s.eq_ignore_ascii_case("enable");
11357 self.advance(); // ENABLE/DISABLE
11358 self.expect_row_level_security()?;
11359 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11360 enabled: Some(enabled),
11361 force: None,
11362 }])
11363 }
11364 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11365 self.advance();
11366 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11367 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11368 // emits. The same grammar CREATE TABLE already accepts
11369 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11370 // through the SAME parser — an ALTER-only copy would be a
11371 // second place for the two to drift.
11372 if self.peek_mysql_inline_key_start() {
11373 return Ok(match self.parse_mysql_inline_key()? {
11374 Some(c) => {
11375 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11376 }
11377 // FULLTEXT / SPATIAL parse and are accepted as a
11378 // no-op here exactly as they are inline.
11379 None => Vec::new(),
11380 });
11381 }
11382 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11383 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11384 // PRIMARY KEY this way; mysqldump emits both.
11385 // Peek-only dispatch (no advance) — `advance()`
11386 // destructively replaces consumed tokens with Eof,
11387 // so saved-pos restore would land on Eofs.
11388 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11389 {
11390 // The next-but-one ident is the constraint
11391 // name; the one after THAT is the kind.
11392 let kind_pos = self.pos + 2;
11393 let kind = self.tokens.get(kind_pos).cloned();
11394 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11395 {
11396 let fk = self.parse_table_level_fk()?;
11397 return Ok(alloc::vec![
11398 crate::ast::AlterTableTarget::AddForeignKey(fk)
11399 ]);
11400 }
11401 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11402 {
11403 self.advance(); // CONSTRAINT
11404 // v7.39 (read01 round 48) — keep the name; the engine
11405 // stores it now instead of dropping it on the floor.
11406 let con_name = self.expect_ident_like()?;
11407 self.advance(); // PRIMARY
11408 self.expect_keyword_ident("key")?;
11409 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11410 // v7.39 (round 711) — the ALTER form carries the
11411 // timing too (pg_dump writes it here).
11412 let (deferrable, initially_deferred) =
11413 self.consume_deferrable_clauses_timed()?;
11414 return Ok(alloc::vec![
11415 crate::ast::AlterTableTarget::AddTableConstraint(
11416 crate::ast::TableConstraint::PrimaryKey {
11417 name: Some(con_name),
11418 columns: cols,
11419 deferrable,
11420 initially_deferred,
11421 }
11422 )
11423 ]);
11424 }
11425 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11426 {
11427 self.advance(); // CONSTRAINT
11428 // v7.39 (read01 round 48) — keep the name.
11429 let con_name = self.expect_ident_like()?;
11430 // v7.22 (mailrs round-13 gap 6) — delegate so
11431 // the optional `NULLS [NOT] DISTINCT` modifier
11432 // parses here too (pg_dump emits the ALTER
11433 // form; semantics enforced by the engine
11434 // since v7.13).
11435 let mut uc = self.parse_table_level_unique()?;
11436 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11437 *name = Some(con_name);
11438 }
11439 return Ok(alloc::vec![
11440 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11441 ]);
11442 }
11443 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11444 {
11445 self.advance(); // CONSTRAINT
11446 // v7.39 (read01 round 48) — keep the name.
11447 let con_name = self.expect_ident_like()?;
11448 self.advance(); // CHECK
11449 if !matches!(self.peek(), Token::LParen) {
11450 return Err(self.err(alloc::format!(
11451 "expected '(' after CHECK, got {:?}", self.peek()
11452 )));
11453 }
11454 self.advance();
11455 let expr = self.parse_expr(0)?;
11456 if matches!(self.peek(), Token::RParen) {
11457 self.advance();
11458 }
11459 let not_valid = self.parse_not_valid_suffix();
11460 return Ok(alloc::vec![
11461 crate::ast::AlterTableTarget::AddTableConstraint(
11462 crate::ast::TableConstraint::Check {
11463 name: Some(con_name),
11464 expr,
11465 not_valid,
11466 }
11467 )
11468 ]);
11469 }
11470 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11471 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11472 // exclusion constraints via this ALTER form.
11473 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11474 {
11475 self.advance(); // CONSTRAINT
11476 let con_name = self.expect_ident_like()?;
11477 let mut ex = self.parse_table_level_exclude()?;
11478 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11479 *name = Some(con_name);
11480 }
11481 return Ok(alloc::vec![
11482 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11483 ]);
11484 }
11485 // Unknown kind — fall through to FK path which
11486 // produces a descriptive parse error.
11487 }
11488 let is_fk = matches!(
11489 self.peek(),
11490 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11491 || s.eq_ignore_ascii_case("foreign")
11492 );
11493 if is_fk {
11494 let fk = self.parse_table_level_fk()?;
11495 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11496 }
11497 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11498 // (no CONSTRAINT prefix) — same dispatch.
11499 match self.peek().clone() {
11500 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11501 self.advance();
11502 self.expect_keyword_ident("key")?;
11503 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11504 let (deferrable, initially_deferred) =
11505 self.consume_deferrable_clauses_timed()?;
11506 return Ok(alloc::vec![
11507 crate::ast::AlterTableTarget::AddTableConstraint(
11508 crate::ast::TableConstraint::PrimaryKey {
11509 name: None,
11510 columns: cols,
11511 deferrable,
11512 initially_deferred,
11513 }
11514 )
11515 ]);
11516 }
11517 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11518 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11519 let uc = self.parse_table_level_unique()?;
11520 return Ok(alloc::vec![
11521 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11522 ]);
11523 }
11524 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11525 // prefix). The other three bare forms were here and
11526 // this one was not, so it fell through to the column
11527 // path and came back as "unexpected reserved keyword
11528 // 'check' at start of column definition".
11529 _ if self.peek_table_level_check_start() => {
11530 let chk = self.parse_table_level_check()?;
11531 let not_valid = self.parse_not_valid_suffix();
11532 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11533 unreachable!("parse_table_level_check returns Check")
11534 };
11535 return Ok(alloc::vec![
11536 crate::ast::AlterTableTarget::AddTableConstraint(
11537 crate::ast::TableConstraint::Check {
11538 name: None,
11539 expr,
11540 not_valid,
11541 }
11542 )
11543 ]);
11544 }
11545 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11546 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11547 let ex = self.parse_table_level_exclude()?;
11548 return Ok(alloc::vec![
11549 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11550 ]);
11551 }
11552 _ => {}
11553 }
11554 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11555 self.advance();
11556 }
11557 let mut if_not_exists = false;
11558 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11559 self.advance();
11560 if !matches!(self.peek(), Token::Not) {
11561 return Err(self.err(alloc::format!(
11562 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11563 self.peek()
11564 )));
11565 }
11566 self.advance();
11567 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11568 return Err(self.err(alloc::format!(
11569 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11570 self.peek()
11571 )));
11572 }
11573 self.advance();
11574 if_not_exists = true;
11575 }
11576 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11577 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11578 // returns ColumnDef + an optional inline FK.
11579 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11580 let col_name = column.name.clone();
11581 // v7.39.9 — MySQL says where the column goes.
11582 let position = self.parse_column_position();
11583 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11584 column,
11585 if_not_exists,
11586 position,
11587 }];
11588 if let Some(mut fk) = col_level_fk {
11589 if fk.columns.is_empty() {
11590 fk.columns.push(col_name);
11591 }
11592 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11593 }
11594 Ok(out)
11595 }
11596 Token::Drop => {
11597 self.advance();
11598 // v7.13.3 — dispatch on the next token. mailrs round-7
11599 // S8 closed DROP COLUMN; round-6 S7 closed
11600 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11601 // RESTRICT modifiers.
11602 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11603 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11604 let subject = match self.peek() {
11605 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11606 self.advance();
11607 "constraint"
11608 }
11609 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11610 self.advance();
11611 "column"
11612 }
11613 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11614 // `INDEX` lexes as the reserved Token::Index, so it is
11615 // unambiguous. `KEY` is a plain ident, and PG allows a
11616 // column literally named "key", so only read it as the
11617 // keyword when a name follows it.
11618 Token::Index => {
11619 self.advance();
11620 "index"
11621 }
11622 Token::Ident(s)
11623 if s.eq_ignore_ascii_case("key")
11624 && matches!(
11625 self.tokens.get(self.pos + 1),
11626 Some(Token::Ident(_) | Token::QuotedIdent(_))
11627 ) =>
11628 {
11629 self.advance();
11630 "index"
11631 }
11632 // PG-canonical bare `DROP <col>` without COLUMN
11633 // keyword is also valid; treat any other ident
11634 // as the column name.
11635 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11636 other => {
11637 return Err(self.err(alloc::format!(
11638 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11639 )));
11640 }
11641 };
11642 let mut if_exists = false;
11643 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11644 let n1 = self.tokens.get(self.pos + 1);
11645 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11646 self.advance();
11647 self.advance();
11648 if_exists = true;
11649 }
11650 }
11651 let name = self.expect_ident_like()?;
11652 let mut cascade = false;
11653 if matches!(
11654 self.peek(),
11655 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11656 || s.eq_ignore_ascii_case("restrict")
11657 ) {
11658 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11659 {
11660 cascade = true;
11661 }
11662 self.advance();
11663 }
11664 if subject == "index" {
11665 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11666 name,
11667 if_exists,
11668 }])
11669 } else if subject == "constraint" {
11670 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11671 name,
11672 if_exists,
11673 }])
11674 } else {
11675 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11676 column: name,
11677 if_exists,
11678 cascade,
11679 }])
11680 }
11681 }
11682 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11683 self.advance();
11684 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11685 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11686 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11687 // immediately; accept-and-no-op.
11688 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11689 self.advance();
11690 self.consume_until_statement_boundary();
11691 return Ok(Vec::new());
11692 }
11693 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11694 self.advance();
11695 }
11696 let col_name = self.expect_ident_like()?;
11697 match self.peek() {
11698 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11699 self.advance();
11700 }
11701 // v7.14.0 — pg_dump emits BIGSERIAL via
11702 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11703 // nextval('seq')` (the sequence is created
11704 // separately). SPG's BIGSERIAL already uses
11705 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11706 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11707 // engine no-ops by consuming the tail.
11708 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11709 // v7.22 (round-13 T2) — `SET DEFAULT
11710 // nextval('…')` is how pg_dump spells a
11711 // SERIAL column (plain integer in CREATE
11712 // TABLE + this ALTER). It used to be
11713 // swallowed as a no-op, which silently
11714 // STRIPPED auto-increment from imported
11715 // schemas — the first post-import INSERT
11716 // without an explicit id then violated NOT
11717 // NULL. Lower it to the auto-increment
11718 // marker instead.
11719 let is_default_nextval =
11720 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11721 && matches!(
11722 self.tokens.get(self.pos + 2),
11723 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11724 );
11725 if is_default_nextval {
11726 let seq_name = self.scan_sequence_name_until_boundary();
11727 return Ok(alloc::vec![
11728 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11729 column: col_name,
11730 seq_name,
11731 }
11732 ]);
11733 }
11734 // v7.37.18 (18.1 + 18.2) — proper lowering.
11735 self.advance(); // consume "set"
11736 match self.peek().clone() {
11737 Token::Default => {
11738 self.advance();
11739 let default_expr = self.parse_expr(0)?;
11740 return Ok(alloc::vec![
11741 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11742 column: col_name,
11743 default_expr,
11744 }
11745 ]);
11746 }
11747 Token::Not => {
11748 self.advance();
11749 if !matches!(self.peek(), Token::Null) {
11750 return Err(self.err(alloc::format!(
11751 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11752 self.peek()
11753 )));
11754 }
11755 self.advance();
11756 return Ok(alloc::vec![
11757 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11758 column: col_name,
11759 }
11760 ]);
11761 }
11762 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11763 // stored generated column's expression and
11764 // recompute existing rows.
11765 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11766 self.advance(); // EXPRESSION
11767 if matches!(self.peek(), Token::As) {
11768 self.advance();
11769 }
11770 let expr = self.parse_expr(0)?;
11771 return Ok(alloc::vec![
11772 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11773 column: col_name,
11774 expr,
11775 }
11776 ]);
11777 }
11778 other => {
11779 // Other SET subjects (STATISTICS,
11780 // STORAGE, COMPRESSION, …) stay no-ops —
11781 // storage hints with no SPG semantics.
11782 let _ = other;
11783 self.consume_until_statement_boundary();
11784 return Ok(Vec::new());
11785 }
11786 }
11787 }
11788 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11789 self.advance(); // consume "drop"
11790 return self.parse_alter_column_drop_tail(col_name);
11791 }
11792 Token::Drop => {
11793 self.advance(); // consume Drop token
11794 return self.parse_alter_column_drop_tail(col_name);
11795 }
11796 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11797 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11798 // GENERATED { ALWAYS | BY DEFAULT } AS
11799 // IDENTITY ( … )`: pg_dump's spelling for
11800 // identity columns. Same auto-increment
11801 // lowering as the nextval default; the
11802 // sequence options inside the parens are
11803 // no-ops under SPG's max+1 semantics.
11804 let is_generated = matches!(
11805 self.tokens.get(self.pos + 1),
11806 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11807 );
11808 if !is_generated {
11809 return Err(self.err(alloc::format!(
11810 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11811 self.tokens.get(self.pos + 1)
11812 )));
11813 }
11814 let seq_name = self.scan_sequence_name_until_boundary();
11815 return Ok(alloc::vec![
11816 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11817 column: col_name,
11818 seq_name,
11819 }
11820 ]);
11821 }
11822 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11823 // column: floor the next allocated value at n (bare
11824 // RESTART = restart from the start value, 1).
11825 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11826 self.advance();
11827 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11828 {
11829 self.advance();
11830 let neg = if matches!(self.peek(), Token::Minus) {
11831 self.advance();
11832 true
11833 } else {
11834 false
11835 };
11836 match self.advance() {
11837 Token::Integer(v) => Some(if neg { -v } else { v }),
11838 other => {
11839 return Err(self.err(alloc::format!(
11840 "expected integer after RESTART WITH, got {other:?}"
11841 )));
11842 }
11843 }
11844 } else {
11845 None
11846 };
11847 return Ok(alloc::vec![
11848 crate::ast::AlterTableTarget::AlterColumnRestart {
11849 column: col_name,
11850 with,
11851 }
11852 ]);
11853 }
11854 other => {
11855 return Err(self.err(alloc::format!(
11856 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11857 )));
11858 }
11859 }
11860 // v7.39 (round 713) — the type parser has consumed a
11861 // trailing `COLLATE <name>` since Phase 2.5, and
11862 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11863 // TYPE text COLLATE "C"` parsed clean and changed
11864 // nothing. Keep the clause; the engine re-collates.
11865 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11866 self.parse_type_with_implied_flags()?;
11867 let collation = if coll_explicit {
11868 coll_name.map(|n| (coll, n))
11869 } else {
11870 None
11871 };
11872 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11873 {
11874 self.advance();
11875 Some(self.parse_expr(0)?)
11876 } else {
11877 None
11878 };
11879 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11880 column: col_name,
11881 new_type,
11882 using,
11883 collation,
11884 }])
11885 }
11886 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11887 // PG also supports `RENAME TO new_table` for table-name
11888 // rename; that surface is deferred (pg_dump never emits
11889 // it). If the first post-RENAME ident is `TO`, the user
11890 // is asking for table rename — error with a clear
11891 // message rather than misparsing `TO` as a column name.
11892 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11893 self.advance();
11894 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11895 // table-name rename (mailrs round-10 A.5 — used
11896 // by migrate-042's `RENAME TO email_contacts`).
11897 // `TO` lexes as Token::To.
11898 if matches!(self.peek(), Token::To)
11899 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11900 {
11901 self.advance();
11902 let new = self.expect_ident_like()?;
11903 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11904 new,
11905 }]);
11906 }
11907 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11908 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11909 self.advance();
11910 let old = self.expect_ident_like()?;
11911 if matches!(self.peek(), Token::To) {
11912 self.advance();
11913 } else {
11914 self.expect_keyword_ident("to")?;
11915 }
11916 let new = self.expect_ident_like()?;
11917 return Ok(alloc::vec![
11918 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11919 ]);
11920 }
11921 // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11922 // PostgreSQL renames an index with its own top-level
11923 // `ALTER INDEX`, so this spelling had nowhere to go and
11924 // answered 1064; MySQL 9.7.2 parses it and answers 1176
11925 // when the key is not there.
11926 if matches!(self.peek(), Token::Index)
11927 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11928 {
11929 self.advance();
11930 let old = self.expect_ident_like()?;
11931 if matches!(self.peek(), Token::To) {
11932 self.advance();
11933 } else {
11934 self.expect_keyword_ident("to")?;
11935 }
11936 let new = self.expect_ident_like()?;
11937 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11938 old,
11939 new,
11940 }]);
11941 }
11942 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11943 self.advance();
11944 }
11945 let old = self.expect_ident_like()?;
11946 // `TO` is a reserved keyword token; accept both
11947 // Token::To and Token::Ident("to") for consistency.
11948 if matches!(self.peek(), Token::To) {
11949 self.advance();
11950 } else {
11951 self.expect_keyword_ident("to")?;
11952 }
11953 let new = self.expect_ident_like()?;
11954 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11955 old,
11956 new,
11957 }])
11958 }
11959 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11960 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11961 // every data block with these. Real disable semantics —
11962 // not no-op — because reload correctness assumes the
11963 // triggers don't fire (rows already carry their
11964 // computed values from prod).
11965 Token::Ident(s)
11966 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11967 {
11968 let enabled = s.eq_ignore_ascii_case("enable");
11969 self.advance();
11970 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11971 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11972 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11973 // pg_dump output) — anything else falls through to
11974 // the catch-all error below.
11975 // v7.22 (round-13 T3) — mysqldump wraps every data
11976 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11977 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11978 // maintains indexes incrementally — engine no-op.
11979 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11980 self.advance();
11981 return Ok(Vec::new());
11982 }
11983 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11984 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11985 // to gate triggers on session_replication_role; SPG
11986 // has no replica role, so the prefix is consumed and
11987 // treated identically to the plain ENABLE/DISABLE
11988 // TRIGGER form.
11989 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11990 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11991 {
11992 self.advance();
11993 }
11994 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11995 return Err(self.err(alloc::format!(
11996 "expected TRIGGER after {}, got {:?}",
11997 if enabled { "ENABLE" } else { "DISABLE" },
11998 self.peek()
11999 )));
12000 }
12001 self.advance();
12002 // `ALL` lexes as Token::All (reserved); also
12003 // accept Token::Ident("all") for symmetry.
12004 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
12005 // TRIGGER selectors. USER (= all user triggers) is
12006 // semantically ALL here; REPLICA / ALWAYS gate on
12007 // session_replication_role which SPG doesn't track.
12008 // All map to TriggerSelector::All.
12009 let which = if matches!(self.peek(), Token::All)
12010 || matches!(self.peek(), Token::Ident(s)
12011 if s.eq_ignore_ascii_case("all")
12012 || s.eq_ignore_ascii_case("user")
12013 || s.eq_ignore_ascii_case("replica")
12014 || s.eq_ignore_ascii_case("always"))
12015 {
12016 self.advance();
12017 crate::ast::TriggerSelector::All
12018 } else {
12019 let name = self.expect_ident_like()?;
12020 crate::ast::TriggerSelector::Named(name)
12021 };
12022 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
12023 which,
12024 enabled,
12025 }])
12026 }
12027 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
12028 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
12029 self.advance();
12030 if !matches!(self.peek(), Token::Partition)
12031 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12032 if s.eq_ignore_ascii_case("partition"))
12033 {
12034 return Err(self.err(alloc::format!(
12035 "expected PARTITION after ATTACH, got {:?}",
12036 self.peek()
12037 )));
12038 }
12039 self.advance();
12040 let child = self.expect_ident_like()?;
12041 let bounds = self.parse_partition_bounds_tail()?;
12042 Ok(alloc::vec![
12043 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12044 ])
12045 }
12046 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12047 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12048 self.advance();
12049 if !matches!(self.peek(), Token::Partition)
12050 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12051 if s.eq_ignore_ascii_case("partition"))
12052 {
12053 return Err(self.err(alloc::format!(
12054 "expected PARTITION after DETACH, got {:?}",
12055 self.peek()
12056 )));
12057 }
12058 self.advance();
12059 let child = self.expect_ident_like()?;
12060 let mut concurrently = false;
12061 let mut finalize = false;
12062 loop {
12063 match self.peek().clone() {
12064 Token::Ident(s) | Token::QuotedIdent(s)
12065 if s.eq_ignore_ascii_case("concurrently") =>
12066 {
12067 self.advance();
12068 concurrently = true;
12069 }
12070 Token::Ident(s) | Token::QuotedIdent(s)
12071 if s.eq_ignore_ascii_case("finalize") =>
12072 {
12073 self.advance();
12074 finalize = true;
12075 }
12076 _ => break,
12077 }
12078 }
12079 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12080 child,
12081 concurrently,
12082 finalize,
12083 }])
12084 }
12085 other => Err(self.err(alloc::format!(
12086 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12087 ))),
12088 }
12089 }
12090
12091 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12092 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12093 /// TABLE … ATTACH PARTITION. Shares the same grammar as
12094 /// `parse_partition_of_tail`'s bounds branch.
12095 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12096 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12097 /// lowering each to the respective AlterTableTarget. Any
12098 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12099 /// no-op via consume_until_statement_boundary.
12100 fn parse_alter_column_drop_tail(
12101 &mut self,
12102 col_name: String,
12103 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12104 match self.peek().clone() {
12105 Token::Default => {
12106 self.advance();
12107 Ok(alloc::vec![
12108 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12109 ])
12110 }
12111 Token::Not => {
12112 self.advance();
12113 if !matches!(self.peek(), Token::Null) {
12114 return Err(self.err(alloc::format!(
12115 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12116 self.peek()
12117 )));
12118 }
12119 self.advance();
12120 Ok(alloc::vec![
12121 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12122 ])
12123 }
12124 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12125 // generated column into a plain column.
12126 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12127 self.advance();
12128 // v7.39 (round 187, U10) — IF EXISTS was consumed but
12129 // dropped, so the engine still errored on a plain
12130 // column; PG's semantics are NOTICE + skip.
12131 let mut if_exists = false;
12132 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12133 self.advance();
12134 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12135 self.advance();
12136 if_exists = true;
12137 }
12138 }
12139 Ok(alloc::vec![
12140 crate::ast::AlterTableTarget::AlterColumnDropExpression {
12141 column: col_name,
12142 if_exists,
12143 }
12144 ])
12145 }
12146 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12147 // identity column into a plain column.
12148 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12149 self.advance();
12150 let mut if_exists = false;
12151 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12152 self.advance();
12153 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12154 self.advance();
12155 if_exists = true;
12156 }
12157 }
12158 Ok(alloc::vec![
12159 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12160 column: col_name,
12161 if_exists,
12162 }
12163 ])
12164 }
12165 _ => {
12166 self.consume_until_statement_boundary();
12167 Ok(Vec::new())
12168 }
12169 }
12170 }
12171
12172 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12173 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12174 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12175 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12176 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12177 let mut opts = crate::ast::CopyOptions::default();
12178 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12179 return Ok(opts);
12180 }
12181 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12182 self.advance();
12183 }
12184 if matches!(self.peek(), Token::LParen) {
12185 self.advance();
12186 loop {
12187 self.parse_one_copy_option(&mut opts)?;
12188 match self.peek() {
12189 Token::Comma => {
12190 self.advance();
12191 }
12192 Token::RParen => {
12193 self.advance();
12194 break;
12195 }
12196 other => {
12197 return Err(self.err(alloc::format!(
12198 "expected ',' or ')' in COPY options, got {other:?}"
12199 )));
12200 }
12201 }
12202 }
12203 } else {
12204 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12205 self.parse_one_copy_option(&mut opts)?;
12206 }
12207 }
12208 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12209 return Err(self.err(alloc::format!(
12210 "unexpected token after COPY options: {:?}",
12211 self.peek()
12212 )));
12213 }
12214 Ok(opts)
12215 }
12216
12217 fn parse_one_copy_option(
12218 &mut self,
12219 opts: &mut crate::ast::CopyOptions,
12220 ) -> Result<(), ParseError> {
12221 use crate::ast::CopyFormat;
12222 // The option keyword. NULL lexes as its own token; the rest are
12223 // bare identifiers.
12224 let kw = match self.advance() {
12225 Token::Null => alloc::string::String::from("NULL"),
12226 Token::Ident(s) => s.to_uppercase(),
12227 other => {
12228 return Err(self.err(alloc::format!(
12229 "expected a COPY option keyword, got {other:?}"
12230 )));
12231 }
12232 };
12233 match kw.as_str() {
12234 "FORMAT" => {
12235 let fmt = self.expect_ident_like()?;
12236 match fmt.to_ascii_uppercase().as_str() {
12237 "CSV" => opts.format = CopyFormat::Csv,
12238 "TEXT" => opts.format = CopyFormat::Text,
12239 other => {
12240 return Err(self.err(alloc::format!(
12241 "COPY format \"{}\" not recognized",
12242 other.to_ascii_lowercase()
12243 )));
12244 }
12245 }
12246 }
12247 // Legacy bare format keywords.
12248 "CSV" => opts.format = CopyFormat::Csv,
12249 "TEXT" => opts.format = CopyFormat::Text,
12250 "HEADER" => {
12251 opts.header = match self.peek() {
12252 Token::True => {
12253 self.advance();
12254 true
12255 }
12256 Token::False => {
12257 self.advance();
12258 false
12259 }
12260 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12261 self.advance();
12262 true
12263 }
12264 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12265 self.advance();
12266 false
12267 }
12268 // Bare HEADER (no boolean) means HEADER true.
12269 _ => true,
12270 };
12271 }
12272 // r1066 (7.38 S5.1) — pgbench 14+ loads with
12273 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12274 // vacuum bookkeeping on a freshly created/truncated
12275 // table; SPG's per-statement visibility makes it a
12276 // faithful no-op, and rejecting it aborted `pgbench -i`
12277 // against the drop-in. Accept ON/OFF/bare, change nothing.
12278 "FREEZE" => match self.peek() {
12279 Token::True | Token::False => {
12280 self.advance();
12281 }
12282 Token::Ident(s)
12283 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12284 {
12285 self.advance();
12286 }
12287 _ => {}
12288 },
12289 "DELIMITER" | "QUOTE" | "ESCAPE" => {
12290 let s = match self.advance() {
12291 Token::String(s) => s,
12292 other => {
12293 return Err(self.err(alloc::format!(
12294 "COPY {kw} expects a single-character string, got {other:?}"
12295 )));
12296 }
12297 };
12298 // v7.39 (round 247) — PG's wording (0A000), keyword in
12299 // lowercase: "COPY delimiter must be a single one-byte
12300 // character".
12301 let one_byte_err = || {
12302 self.err(alloc::format!(
12303 "COPY {} must be a single one-byte character",
12304 kw.to_ascii_lowercase()
12305 ))
12306 };
12307 let mut chars = s.chars();
12308 let c = chars.next().ok_or_else(one_byte_err)?;
12309 if chars.next().is_some() || c.len_utf8() != 1 {
12310 return Err(one_byte_err());
12311 }
12312 match kw.as_str() {
12313 "DELIMITER" => opts.delimiter = Some(c),
12314 "QUOTE" => opts.quote = Some(c),
12315 _ => opts.escape = Some(c),
12316 }
12317 }
12318 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12319 "FORCE_QUOTE" => {
12320 if matches!(self.peek(), Token::Star) {
12321 self.advance();
12322 opts.force_quote = Some(Vec::new());
12323 } else {
12324 if !matches!(self.peek(), Token::LParen) {
12325 return Err(self.err(alloc::format!(
12326 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12327 self.peek()
12328 )));
12329 }
12330 self.advance();
12331 let mut cols = Vec::new();
12332 loop {
12333 cols.push(self.expect_ident_like()?);
12334 match self.peek() {
12335 Token::Comma => {
12336 self.advance();
12337 }
12338 Token::RParen => {
12339 self.advance();
12340 break;
12341 }
12342 other => {
12343 return Err(self.err(alloc::format!(
12344 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12345 )));
12346 }
12347 }
12348 }
12349 opts.force_quote = Some(cols);
12350 }
12351 }
12352 "NULL" => {
12353 opts.null_str = Some(match self.advance() {
12354 Token::String(s) => s,
12355 other => {
12356 return Err(self.err(alloc::format!(
12357 "COPY NULL expects a quoted string, got {other:?}"
12358 )));
12359 }
12360 });
12361 }
12362 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12363 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12364 // FORCE_NULL too.
12365 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12366 let cols = self.parse_copy_column_list(&kw)?;
12367 if kw == "FORCE_NOT_NULL" {
12368 opts.force_not_null = Some(cols);
12369 } else {
12370 opts.force_null = Some(cols);
12371 }
12372 }
12373 other => {
12374 // PG's wording, lowercased option name.
12375 return Err(self.err(alloc::format!(
12376 "option \"{}\" not recognized",
12377 other.to_ascii_lowercase()
12378 )));
12379 }
12380 }
12381 Ok(())
12382 }
12383
12384 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12385 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12386 /// is the `*` spelling.
12387 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12388 if matches!(self.peek(), Token::Star) {
12389 self.advance();
12390 return Ok(Vec::new());
12391 }
12392 if !matches!(self.peek(), Token::LParen) {
12393 return Err(self.err(alloc::format!(
12394 "expected '(' or '*' after {kw}, got {:?}",
12395 self.peek()
12396 )));
12397 }
12398 self.advance();
12399 let mut cols = Vec::new();
12400 loop {
12401 cols.push(self.expect_ident_like()?);
12402 match self.peek() {
12403 Token::Comma => {
12404 self.advance();
12405 }
12406 Token::RParen => {
12407 self.advance();
12408 break;
12409 }
12410 other => {
12411 return Err(self.err(alloc::format!(
12412 "expected ',' or ')' in {kw} list, got {other:?}"
12413 )));
12414 }
12415 }
12416 }
12417 Ok(cols)
12418 }
12419
12420 fn parse_partition_bounds_tail(
12421 &mut self,
12422 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12423 use crate::ast::PartitionOfBoundsAst;
12424 match self.peek() {
12425 Token::Default => {
12426 self.advance();
12427 Ok(PartitionOfBoundsAst::Default)
12428 }
12429 Token::For => {
12430 self.advance();
12431 if !matches!(self.peek(), Token::Values) {
12432 return Err(
12433 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12434 );
12435 }
12436 self.advance();
12437 let want_with = matches!(
12438 self.peek(),
12439 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12440 );
12441 if want_with {
12442 self.advance();
12443 if !matches!(self.peek(), Token::LParen) {
12444 return Err(self.err(format!(
12445 "expected '(' after FOR VALUES WITH, got {:?}",
12446 self.peek()
12447 )));
12448 }
12449 self.advance();
12450 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12451 loop {
12452 let key = self.expect_ident_like()?;
12453 let n = match self.peek().clone() {
12454 Token::Integer(v) if u32::try_from(v).is_ok() => {
12455 self.advance();
12456 v as u32
12457 }
12458 other => {
12459 return Err(self.err(format!(
12460 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12461 )));
12462 }
12463 };
12464 match key.to_ascii_uppercase().as_str() {
12465 "MODULUS" => modulus = Some(n),
12466 "REMAINDER" => remainder = Some(n),
12467 other => {
12468 return Err(self.err(format!(
12469 "FOR VALUES WITH: unknown key {other:?}; \
12470 expected MODULUS or REMAINDER"
12471 )));
12472 }
12473 }
12474 match self.peek() {
12475 Token::Comma => {
12476 self.advance();
12477 }
12478 Token::RParen => {
12479 self.advance();
12480 break;
12481 }
12482 other => {
12483 return Err(self.err(format!(
12484 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12485 )));
12486 }
12487 }
12488 }
12489 let modulus = modulus
12490 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12491 let remainder = remainder.ok_or_else(|| {
12492 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12493 })?;
12494 if modulus == 0 {
12495 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12496 }
12497 if remainder >= modulus {
12498 return Err(self.err(format!(
12499 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12500 )));
12501 }
12502 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12503 }
12504 match self.peek() {
12505 Token::From => {
12506 self.advance();
12507 let lower = Box::new(self.parse_partition_bound_expr()?);
12508 if !matches!(self.peek(), Token::To) {
12509 return Err(self.err(format!(
12510 "expected TO after FROM (...), got {:?}",
12511 self.peek()
12512 )));
12513 }
12514 self.advance();
12515 let upper = Box::new(self.parse_partition_bound_expr()?);
12516 Ok(PartitionOfBoundsAst::Range { lower, upper })
12517 }
12518 Token::In => {
12519 self.advance();
12520 if !matches!(self.peek(), Token::LParen) {
12521 return Err(self.err(format!(
12522 "expected '(' after FOR VALUES IN, got {:?}",
12523 self.peek()
12524 )));
12525 }
12526 self.advance();
12527 let mut values = Vec::new();
12528 loop {
12529 values.push(self.parse_expr(0)?);
12530 match self.peek() {
12531 Token::Comma => {
12532 self.advance();
12533 }
12534 Token::RParen => {
12535 self.advance();
12536 break;
12537 }
12538 other => {
12539 return Err(self.err(format!(
12540 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12541 )));
12542 }
12543 }
12544 }
12545 if values.is_empty() {
12546 return Err(
12547 self.err("FOR VALUES IN requires at least one literal".to_string())
12548 );
12549 }
12550 Ok(PartitionOfBoundsAst::List { values })
12551 }
12552 other => Err(self.err(format!(
12553 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12554 ))),
12555 }
12556 }
12557 other => Err(self.err(format!(
12558 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12559 ))),
12560 }
12561 }
12562
12563 /// v7.16.2 — peek for `information_schema.<tbl>` /
12564 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12565 /// three tokens + return a synthetic table name the engine's
12566 /// SELECT path recognises as a virtual view. Returns `None`
12567 /// when the head doesn't look like a meta-qualified name.
12568 /// Used by `parse_table_ref` to bypass the
12569 /// `expect_ident_like` schema-strip for these specific PG
12570 /// meta schemas (mailrs round-10 A.3).
12571 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12572 // Extract the schema name. Must be a plain ident token.
12573 let schema = match self.tokens.get(self.pos) {
12574 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12575 _ => return None,
12576 };
12577 // Dot.
12578 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12579 return None;
12580 }
12581 // The table-side ident may lex as a reserved keyword
12582 // (e.g. `Token::Tables`). Tolerate the common ones via a
12583 // helper that reads the trailing token's underlying name.
12584 let tbl = match self.tokens.get(self.pos + 2)? {
12585 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12586 Token::Tables => "tables".to_string(),
12587 // Other PG meta table names that may collide with
12588 // reserved keywords land here as needed.
12589 _ => return None,
12590 };
12591 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12592 // names so the synthetic name doesn't double-prefix
12593 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12594 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12595 ("__spg_info_", tbl.to_ascii_lowercase())
12596 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12597 // v7.39 (round 541) — only the catalogs SPG actually
12598 // synthesises are rewritten, which is what the BARE path
12599 // has always checked. Anything else keeps its own name and
12600 // takes the ordinary route: `pg_stat_activity` and friends
12601 // resolve through meta_view_result, and a name that is no
12602 // catalog at all gets PG's "relation does not exist"
12603 // instead of a message about a view SPG cannot materialise.
12604 let lowered = tbl.to_ascii_lowercase();
12605 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12606 self.advance(); // schema
12607 self.advance(); // dot
12608 self.advance(); // tbl
12609 return Some((lowered.clone(), lowered));
12610 }
12611 let bare = lowered
12612 .strip_prefix("pg_")
12613 .map(alloc::string::String::from)
12614 .unwrap_or(lowered);
12615 ("__spg_pg_", bare)
12616 } else if schema.eq_ignore_ascii_case("mysql") {
12617 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12618 // (`mysql.user`, `mysql.db`). Same synthetic-name
12619 // shape as pg_catalog.
12620 ("__spg_mysql_", tbl.to_ascii_lowercase())
12621 } else {
12622 return None;
12623 };
12624 self.advance(); // schema
12625 self.advance(); // dot
12626 self.advance(); // tbl
12627 Some((
12628 alloc::format!("{prefix}{normalised}"),
12629 tbl.to_ascii_lowercase(),
12630 ))
12631 }
12632
12633 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12634 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12635 /// implicit front of every search_path, so a bare reference to a
12636 /// known catalog table always means the catalog table. Only the
12637 /// names the engine actually synthesises are recognised — any
12638 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12639 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12640 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12641 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12642 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12643 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12644 // through the meta_view_result path instead, and already resolve
12645 // bare — they must NOT be listed here or the __spg_ rewrite would
12646 // mis-target them.)
12647 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12648 let name = match self.tokens.get(self.pos) {
12649 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12650 _ => return None,
12651 };
12652 // A following dot means this ident is a schema qualifier,
12653 // not a table name — let the qualified path handle it.
12654 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12655 return None;
12656 }
12657 if !PG_META_TABLES.contains(&name.as_str()) {
12658 return None;
12659 }
12660 self.advance();
12661 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12662 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12663 }
12664
12665 /// Consume a bare ident if its lowercase matches `kw`, else err.
12666 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12667 /// Peeks only; the caller advances.
12668 fn peek_keyword_ident(&self, kw: &str) -> bool {
12669 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12670 }
12671
12672 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12673 match self.advance() {
12674 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12675 other => Err(ParseError {
12676 message: format!("expected {kw:?}, got {other:?}"),
12677 token_pos: self.consumed_pos(),
12678 }),
12679 }
12680 }
12681
12682 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12683 /// literal (`'foo'`) — same shape used by CREATE USER for the
12684 /// username slot.
12685 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12686 match self.advance() {
12687 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12688 other => Err(ParseError {
12689 message: format!("expected identifier or string, got {other:?}"),
12690 token_pos: self.consumed_pos(),
12691 }),
12692 }
12693 }
12694
12695 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12696 match self.advance() {
12697 Token::String(s) => Ok(s),
12698 other => Err(ParseError {
12699 message: format!("expected quoted string, got {other:?}"),
12700 token_pos: self.consumed_pos(),
12701 }),
12702 }
12703 }
12704
12705 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12706 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12707 // subqueries recurse through here without passing
12708 // parse_expr; share the same nesting budget.
12709 self.enter_nested()?;
12710 let r = self.parse_select_stmt_inner();
12711 self.nest_depth -= 1;
12712 r
12713 }
12714
12715 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12716 // Caller dispatches on Token::Select; the inner helper handles
12717 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12718 // get a fresh bare-select parse and may not have their own ORDER
12719 // BY / LIMIT.
12720 let mut head = self.parse_bare_select()?;
12721 let into = self.pending_select_into.take();
12722 self.parse_setop_chain_into(&mut head)?;
12723 self.parse_select_tail_into(&mut head)?;
12724 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12725 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12726 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12727 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12728 // to the body, as it does in PostgreSQL.
12729 if let Some((name, temporary)) = into {
12730 return Ok(Statement::CreateMaterializedView(
12731 crate::ast::CreateMaterializedViewStatement {
12732 temporary,
12733 name,
12734 if_not_exists: false,
12735 columns: Vec::new(),
12736 body: head,
12737 with_data: true,
12738 as_plain_table: true,
12739 },
12740 ));
12741 }
12742 Ok(Statement::Select(head))
12743 }
12744
12745 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12746 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12747 /// token), and INTERSECT [ALL] (a bare ident — it was never
12748 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12749 /// tighter than UNION / EXCEPT — the executor folds the chain
12750 /// left-to-right, which is already correct for LEADING
12751 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12752 /// pair nests into that previous peer, so A UNION B INTERSECT C
12753 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12754 /// groups.
12755 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12756 // A parenthesized group arrives with its own (already
12757 // regrouped) unions on `head`; only the pairs THIS chain
12758 // appends participate in the precedence regroup below —
12759 // nesting an outer INTERSECT into a group-internal peer
12760 // would dissolve the explicit grouping.
12761 let boundary = head.unions.len();
12762 loop {
12763 let base = match self.peek() {
12764 Token::Union => UnionKind::Distinct,
12765 Token::Except => UnionKind::Except,
12766 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12767 _ => break,
12768 };
12769 self.advance();
12770 let kind = if matches!(self.peek(), Token::All) {
12771 self.advance();
12772 match base {
12773 UnionKind::Distinct => UnionKind::All,
12774 UnionKind::Except => UnionKind::ExceptAll,
12775 _ => UnionKind::IntersectAll,
12776 }
12777 } else {
12778 base
12779 };
12780 let peer = self.parse_bare_select()?;
12781 head.unions.push((kind, peer));
12782 }
12783 let mut pairs = core::mem::take(&mut head.unions);
12784 let tail = pairs.split_off(boundary);
12785 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12786 for (kind, peer) in tail {
12787 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12788 // An intersect nests into the previous element of THIS
12789 // chain only; with no new previous element it stays at
12790 // the outer level (the left fold applies it to the
12791 // whole head, group included).
12792 match (
12793 is_intersect,
12794 regrouped.len() > boundary,
12795 regrouped.last_mut(),
12796 ) {
12797 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12798 _ => regrouped.push((kind, peer)),
12799 }
12800 }
12801 head.unions = regrouped;
12802 Ok(())
12803 }
12804
12805 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12806 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12807 /// the top-level bare VALUES statement reuses it verbatim.
12808 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12809 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12810 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12811 /// where the grouping-set universe is still in scope.
12812 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12813 if !matches!(self.peek(), Token::Order) {
12814 return Ok(Vec::new());
12815 }
12816 self.advance();
12817 if !self.peek_is_by() {
12818 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12819 }
12820 self.advance();
12821 let mut keys = Vec::new();
12822 loop {
12823 // v7.39 (round 691) — save/restore, the discipline this parser
12824 // already uses around `pending_sample_preds`, so a subquery inside
12825 // a key neither inherits nor leaks the channel.
12826 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12827 let saved_coll = self.order_key_collation.take();
12828 let parsed = self.parse_expr(0);
12829 self.in_order_by_key = saved_flag;
12830 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12831 let expr = parsed?;
12832 let desc = if matches!(self.peek(), Token::Desc) {
12833 self.advance();
12834 true
12835 } else if matches!(self.peek(), Token::Asc) {
12836 self.advance();
12837 false
12838 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12839 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12840 // one ordering per type, so the btree comparison operators map
12841 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12842 // would need a custom operator class — honest error.
12843 self.advance();
12844 match self.advance() {
12845 Token::Lt | Token::LtEq => false,
12846 Token::Gt | Token::GtEq => true,
12847 other => {
12848 return Err(self.err(alloc::format!(
12849 "ORDER BY USING supports the btree comparison \
12850 operators (< <= > >=); got {other:?}"
12851 )));
12852 }
12853 }
12854 } else {
12855 false
12856 };
12857 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12858 let nulls_first = self.parse_optional_nulls_placement()?;
12859 keys.push(OrderBy {
12860 expr,
12861 desc,
12862 nulls_first,
12863 collation,
12864 });
12865 if matches!(self.peek(), Token::Comma) {
12866 self.advance();
12867 } else {
12868 break;
12869 }
12870 }
12871 Ok(keys)
12872 }
12873
12874 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12875 // v7.39 (round 135) — a grouping-set query may have already parsed +
12876 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12877 // no ORDER BY token is present, keep that pre-set order_by rather than
12878 // clobbering it with an empty list.
12879 let parsed_keys = self.parse_order_by_keys()?;
12880 head.order_by = if parsed_keys.is_empty() {
12881 core::mem::take(&mut head.order_by)
12882 } else {
12883 parsed_keys
12884 };
12885 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12886 // order. PG's grammar takes a limit clause and an offset clause
12887 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12888 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12889 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12890 // spelling died on `expected end of input, got Limit`.
12891 //
12892 // Each may appear at most once, and LIMIT and FETCH FIRST are
12893 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12894 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12895 // A second one is left unconsumed here, which the caller reports
12896 // as trailing input rather than silently taking the last.
12897 let mut saw_limit = false;
12898 let mut saw_offset = false;
12899 loop {
12900 if !saw_limit && matches!(self.peek(), Token::Limit) {
12901 self.advance();
12902 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12903 // PG synonyms for "no limit". Treat both as None
12904 // (no head.limit set) so the engine's existing
12905 // unlimited-result path takes over. Reject was the
12906 // pre-5.1 behaviour and broke pg_dump-flavoured
12907 // tooling that occasionally emits LIMIT NULL.
12908 if self.consume_limit_unbounded_sentinel() {
12909 head.limit = None;
12910 } else {
12911 let first = self.parse_limit_expr("LIMIT")?;
12912 // MySQL `LIMIT offset, count` — the first number is
12913 // the offset when a comma follows.
12914 if matches!(self.peek(), Token::Comma) {
12915 self.advance();
12916 let count = self.parse_limit_expr("LIMIT")?;
12917 head.offset = Some(first);
12918 saw_offset = true;
12919 head.limit = Some(count);
12920 } else {
12921 head.limit = Some(first);
12922 }
12923 }
12924 saw_limit = true;
12925 continue;
12926 }
12927 if !saw_offset && matches!(self.peek(), Token::Offset) {
12928 self.advance();
12929 // PG also accepts an optional `ROW` / `ROWS` trailer
12930 // after the offset value (`OFFSET 10 ROWS`). The
12931 // FETCH-FIRST branch below relies on the same.
12932 let off = self.parse_limit_expr("OFFSET")?;
12933 self.consume_optional_rows_keyword();
12934 head.offset = Some(off);
12935 saw_offset = true;
12936 continue;
12937 }
12938 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12939 // the SQL-standard alias for LIMIT. PG accepts both
12940 // spellings interchangeably; pg_dump emits FETCH FIRST in
12941 // newer versions. We map it onto `head.limit` so the
12942 // engine path is unified.
12943 if !saw_limit
12944 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12945 if s.eq_ignore_ascii_case("fetch"))
12946 {
12947 self.advance(); // FETCH
12948 // `FIRST` or `NEXT` (both legal per SQL standard).
12949 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12950 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12951 {
12952 self.advance();
12953 }
12954 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12955 // implicit 1 — but we always consume one if present).
12956 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12957 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12958 {
12959 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12960 crate::ast::LimitExpr::Literal(1)
12961 } else {
12962 self.parse_limit_expr("FETCH FIRST")?
12963 };
12964 // Eat `ROW` / `ROWS` if not already consumed above.
12965 self.consume_optional_rows_keyword();
12966 // Optional `ONLY` (the spec form) — or the SQL:2008
12967 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12968 // now honours WITH TIES by extending past the LIMIT
12969 // truncation point through every row that shares the
12970 // last-kept row's ORDER BY key.
12971 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12972 if s.eq_ignore_ascii_case("only"))
12973 {
12974 self.advance();
12975 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12976 if s.eq_ignore_ascii_case("with"))
12977 {
12978 self.advance(); // WITH
12979 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12980 if s.eq_ignore_ascii_case("ties"))
12981 {
12982 self.advance();
12983 head.limit_with_ties = true;
12984 }
12985 }
12986 head.limit = Some(count);
12987 saw_limit = true;
12988 continue;
12989 }
12990 break;
12991 }
12992 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12993 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12994 // [ OF table_name [, …] ]
12995 // [ NOWAIT | SKIP LOCKED ]
12996 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12997 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12998 // SELECT already returns a consistent snapshot — so these
12999 // are accept-and-discard: the parser absorbs them so
13000 // mailrs / Rails / Django code paths that emit `SELECT
13001 // … FOR UPDATE` for advisory pessimistic locking load
13002 // without a parser error. The on-disk locking model is
13003 // unchanged; callers that rely on FOR UPDATE for read-
13004 // through-write ordering still get the right answer
13005 // because SPG serialises writes anyway.
13006 head.locking = self
13007 .consume_optional_for_lock_clauses()
13008 .map(alloc::boxed::Box::new);
13009 Ok(())
13010 }
13011
13012 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
13013 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
13014 /// LOCKED ]` trailers. Each clause is fully accepted and
13015 /// discarded — SPG's single-writer model already satisfies the
13016 /// callers' implicit ordering requirement. Stops at the first
13017 /// token that isn't `FOR`.
13018 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
13019 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
13020 // not discarded. PG keeps the strongest of several clauses; the
13021 // policy of the last one wins, which is what this loop records.
13022 let mut seen: Option<crate::ast::LockingClause> = None;
13023 while matches!(self.peek(), Token::For) {
13024 // v7.37.14 (A2.5-stub) — record that this query asked
13025 // for a row lock the parser is about to silently
13026 // discard. Operators surface the count via
13027 // `spg_sql::silent_for_update_count()` so they can
13028 // gauge how much of the workload depends on advisory
13029 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13030 // before v7.37.15's per-row tuple locking lands.
13031 crate::record_silent_for_update_clause();
13032 self.advance(); // FOR
13033 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13034 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13035 let mut no_key = false;
13036 let mut key = false;
13037 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13038 if s.eq_ignore_ascii_case("no"))
13039 {
13040 self.advance(); // NO
13041 no_key = true;
13042 // The next ident should be KEY but be generous;
13043 // anything followed by UPDATE/SHARE is accepted.
13044 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13045 if s.eq_ignore_ascii_case("key"))
13046 {
13047 self.advance(); // KEY
13048 }
13049 }
13050 // `KEY` prefix (PG `FOR KEY SHARE`).
13051 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13052 if s.eq_ignore_ascii_case("key"))
13053 {
13054 self.advance(); // KEY
13055 key = true;
13056 }
13057 // Lock-strength keyword: UPDATE / SHARE. Required, but
13058 // we're lenient — an unexpected token here just bails
13059 // (we already consumed FOR; caller's downstream
13060 // dispatch will error if anything actually depends on
13061 // the trailing tokens).
13062 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13063 if s.eq_ignore_ascii_case("update"));
13064 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13065 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13066 {
13067 self.advance();
13068 use crate::ast::LockStrength as LS;
13069 let strength = match (is_update, no_key, key) {
13070 (true, true, _) => LS::NoKeyUpdate,
13071 (true, _, _) => LS::Update,
13072 (false, _, true) => LS::KeyShare,
13073 (false, _, _) => LS::Share,
13074 };
13075 seen = Some(crate::ast::LockingClause {
13076 strength,
13077 of_tables: alloc::vec::Vec::new(),
13078 policy: crate::ast::LockWait::Wait,
13079 });
13080 } else {
13081 // FOR by itself (or `FOR KEY` with nothing after) —
13082 // give up on the lock-clause path. We've already
13083 // advanced past FOR; further attempts to parse
13084 // here would clobber state.
13085 return seen;
13086 }
13087 // Optional `OF tbl[, tbl …]`. mailrs emits this when
13088 // joining and locking only a subset of tables.
13089 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13090 if s.eq_ignore_ascii_case("of"))
13091 {
13092 self.advance(); // OF
13093 #[allow(clippy::while_let_loop)]
13094 loop {
13095 match self.peek() {
13096 Token::Ident(_) | Token::QuotedIdent(_) => {
13097 // v7.39 (round 294) — the name is CAPTURED now: PG
13098 // validates it against the FROM clause, and an
13099 // uncaptured list silently means "lock everything".
13100 let mut nm = match self.advance() {
13101 Token::Ident(n) | Token::QuotedIdent(n) => n,
13102 _ => alloc::string::String::new(),
13103 };
13104 // Optional schema-qualified `schema.table`.
13105 if matches!(self.peek(), Token::Dot) {
13106 self.advance();
13107 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13108 {
13109 self.advance();
13110 nm = n;
13111 }
13112 }
13113 if let Some(c) = seen.as_mut() {
13114 c.of_tables.push(nm);
13115 }
13116 }
13117 _ => break,
13118 }
13119 if matches!(self.peek(), Token::Comma) {
13120 self.advance();
13121 } else {
13122 break;
13123 }
13124 }
13125 }
13126 // Optional `NOWAIT` | `SKIP LOCKED`.
13127 match self.peek().clone() {
13128 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13129 self.advance();
13130 if let Some(c) = seen.as_mut() {
13131 c.policy = crate::ast::LockWait::NoWait;
13132 }
13133 }
13134 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13135 self.advance(); // SKIP
13136 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13137 if s.eq_ignore_ascii_case("locked"))
13138 {
13139 self.advance(); // LOCKED
13140 if let Some(c) = seen.as_mut() {
13141 c.policy = crate::ast::LockWait::SkipLocked;
13142 }
13143 }
13144 }
13145 _ => {}
13146 }
13147 // Loop: PG allows multiple FOR clauses chained.
13148 }
13149 seen
13150 }
13151
13152 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13153 /// Bind value gets resolved during prepared-statement Execute;
13154 /// the Pratt expression parser would over-accept here (e.g.
13155 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13156 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13157 /// sentinel tokens (PG synonyms for "no limit"). Returns true
13158 /// when one was consumed; caller skips the regular
13159 /// limit-value parse and leaves `head.limit` at None.
13160 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13161 if matches!(self.peek(), Token::Null) {
13162 self.advance();
13163 return true;
13164 }
13165 if matches!(self.peek(), Token::All) {
13166 self.advance();
13167 return true;
13168 }
13169 false
13170 }
13171
13172 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13173 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13174 /// SQL-standard shape. No-op when missing.
13175 fn consume_optional_rows_keyword(&mut self) {
13176 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13177 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13178 {
13179 self.advance();
13180 }
13181 }
13182
13183 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13184 ///
13185 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13186 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13187 /// constant, which is why that spelling keeps the token path below.
13188 ///
13189 /// Constants are folded here rather than carried into the tree: the
13190 /// 15+ execution paths that read the row count go through
13191 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13192 /// means "no limit". A clause the engine could not resolve would
13193 /// therefore return the WHOLE table instead of failing. Folding at
13194 /// parse time keeps that impossible; a non-constant clause is still
13195 /// a clean error (recorded residual — closing it wants a resolution
13196 /// pre-pass on the simple-query path, where `substitute_placeholders`
13197 /// does not run).
13198 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13199 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13200 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13201 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13202 // ONLY` both work (its grammar takes a c_expr). Both measured
13203 // against PG 18.4 in round 305.
13204 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13205 return self.parse_limit_constant(label);
13206 }
13207 // One pass, no rewind: `advance()` takes each token by
13208 // `mem::replace`, so a consumed token reads back as Eof and this
13209 // parser cannot backtrack. Everything — bare literal included —
13210 // is therefore folded from the parsed expression rather than
13211 // re-read from the token stream.
13212 let start = self.pos;
13213 let e = self.parse_expr(0)?;
13214 if let crate::ast::Expr::Placeholder(n) = e {
13215 return Ok(crate::ast::LimitExpr::Placeholder(n));
13216 }
13217 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13218 match fold_limit_constant(&e) {
13219 Some(Ok(v)) if v < 0 => Err(ParseError {
13220 message: alloc::format!("{neg_label} must not be negative"),
13221 token_pos: start,
13222 }),
13223 Some(Ok(v)) => u32::try_from(v)
13224 .map(crate::ast::LimitExpr::Literal)
13225 .map_err(|_| ParseError {
13226 message: alloc::format!("{label} value too large: {v}"),
13227 token_pos: start,
13228 }),
13229 Some(Err(message)) => Err(ParseError {
13230 message: message.replace("{L}", neg_label),
13231 token_pos: start,
13232 }),
13233 // v7.39 (round 305, V23) — not foldable at parse time
13234 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13235 // expression; the engine evaluates it once before dispatch.
13236 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13237 }
13238 }
13239
13240 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13241 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13242 // coercion rules, not just an integer token: a NUMERIC rounds half
13243 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13244 // refused with PG's wording ("LIMIT must not be negative", 2201W /
13245 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13246 // content, failing as an input-syntax error on the value. General
13247 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13248 // they need an Expr-carrying LimitExpr variant.
13249 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13250 let err_at = |message: alloc::string::String, pos: usize| ParseError {
13251 message,
13252 token_pos: pos,
13253 };
13254 match self.advance() {
13255 Token::Integer(n) if n >= 0 => u32::try_from(n)
13256 .map(crate::ast::LimitExpr::Literal)
13257 .map_err(|_| ParseError {
13258 message: alloc::format!("{label} value too large: {n}"),
13259 token_pos: self.consumed_pos(),
13260 }),
13261 Token::Integer(_) => Err(err_at(
13262 alloc::format!("{neg_label} must not be negative"),
13263 self.pos.saturating_sub(1),
13264 )),
13265 Token::Numeric(t) => {
13266 let pos = self.pos.saturating_sub(1);
13267 let v: f64 = t.parse().map_err(|_| {
13268 err_at(
13269 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13270 pos,
13271 )
13272 })?;
13273 if v < 0.0 {
13274 return Err(err_at(
13275 alloc::format!("{neg_label} must not be negative"),
13276 pos,
13277 ));
13278 }
13279 // Round half away from zero — PG's numeric→bigint cast.
13280 // (no_std: no f64::round; v is non-negative, so truncating
13281 // v + 0.5 is the same thing.)
13282 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13283 let rounded = (v + 0.5) as u64;
13284 u32::try_from(rounded)
13285 .map(crate::ast::LimitExpr::Literal)
13286 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13287 }
13288 Token::Minus => {
13289 let pos = self.pos.saturating_sub(1);
13290 match self.peek() {
13291 Token::Integer(_) | Token::Numeric(_) => {
13292 self.advance();
13293 Err(err_at(
13294 alloc::format!("{neg_label} must not be negative"),
13295 pos,
13296 ))
13297 }
13298 other => Err(err_at(
13299 alloc::format!(
13300 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13301 ),
13302 pos,
13303 )),
13304 }
13305 }
13306 Token::String(t) => {
13307 let pos = self.pos.saturating_sub(1);
13308 match t.trim().parse::<i64>() {
13309 Ok(n) if n < 0 => Err(err_at(
13310 alloc::format!("{neg_label} must not be negative"),
13311 pos,
13312 )),
13313 Ok(n) => u32::try_from(n)
13314 .map(crate::ast::LimitExpr::Literal)
13315 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13316 Err(_) => Err(err_at(
13317 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13318 pos,
13319 )),
13320 }
13321 }
13322 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13323 other => Err(ParseError {
13324 message: alloc::format!(
13325 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13326 ),
13327 token_pos: self.consumed_pos(),
13328 }),
13329 }
13330 }
13331
13332 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13333 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13334 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13335 /// `parse_select_stmt` is responsible for filling those in.
13336 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13337 /// call in the expression tree to the per-set integer bitmask
13338 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13339 /// is dropped in this grouping set). Runs during the ROLLUP /
13340 /// CUBE / GROUPING SETS expansion, where the set is known.
13341 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13342 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13343 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13344 if let Expr::FunctionCall { name, .. } = expr
13345 && name.eq_ignore_ascii_case("grouping")
13346 {
13347 if !out.iter().any(|e| e == expr) {
13348 out.push(expr.clone());
13349 }
13350 return;
13351 }
13352 match expr {
13353 Expr::Binary { lhs, rhs, .. } => {
13354 Self::collect_grouping_calls(lhs, out);
13355 Self::collect_grouping_calls(rhs, out);
13356 }
13357 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13358 Self::collect_grouping_calls(expr, out)
13359 }
13360 Expr::FunctionCall { args, .. } => {
13361 for a in args {
13362 Self::collect_grouping_calls(a, out);
13363 }
13364 }
13365 Expr::Case {
13366 operand,
13367 branches,
13368 else_branch,
13369 } => {
13370 if let Some(o) = operand {
13371 Self::collect_grouping_calls(o, out);
13372 }
13373 for (c, v) in branches {
13374 Self::collect_grouping_calls(c, out);
13375 Self::collect_grouping_calls(v, out);
13376 }
13377 if let Some(x) = else_branch {
13378 Self::collect_grouping_calls(x, out);
13379 }
13380 }
13381 _ => {}
13382 }
13383 }
13384
13385 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13386 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13387 /// `__grp_ord_k` (injected per grouping-set branch).
13388 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13389 if let Expr::FunctionCall { name, .. } = expr
13390 && name.eq_ignore_ascii_case("grouping")
13391 {
13392 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13393 *expr = Expr::Column(crate::ast::ColumnName {
13394 qualifier: None,
13395 name: alloc::format!("__grp_ord_{k}"),
13396 });
13397 }
13398 return;
13399 }
13400 match expr {
13401 Expr::Binary { lhs, rhs, .. } => {
13402 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13403 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13404 }
13405 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13406 Self::rewrite_grouping_to_col(expr, grp_exprs)
13407 }
13408 Expr::FunctionCall { args, .. } => {
13409 for a in args {
13410 Self::rewrite_grouping_to_col(a, grp_exprs);
13411 }
13412 }
13413 Expr::Case {
13414 operand,
13415 branches,
13416 else_branch,
13417 } => {
13418 if let Some(o) = operand {
13419 Self::rewrite_grouping_to_col(o, grp_exprs);
13420 }
13421 for (c, v) in branches {
13422 Self::rewrite_grouping_to_col(c, grp_exprs);
13423 Self::rewrite_grouping_to_col(v, grp_exprs);
13424 }
13425 if let Some(x) = else_branch {
13426 Self::rewrite_grouping_to_col(x, grp_exprs);
13427 }
13428 }
13429 _ => {}
13430 }
13431 }
13432
13433 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13434 /// as the list of key sets it contributes. A bare expression is one
13435 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13436 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13437 /// the concatenation of its items' sets, where an item is itself an
13438 /// element, a parenthesized key list, or the empty set `()`. A
13439 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13440 /// move together.
13441 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13442 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13443 // ROLLUP ( … ) / CUBE ( … )
13444 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13445 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13446 {
13447 let is_cube = is_kw(self.peek(), "cube");
13448 self.advance(); // ROLLUP / CUBE
13449 self.advance(); // (
13450 let mut units: Vec<Vec<Expr>> = Vec::new();
13451 loop {
13452 if matches!(self.peek(), Token::LParen) {
13453 // Composite unit: (a, b) rolls up as one.
13454 self.advance();
13455 let mut unit = Vec::new();
13456 if !matches!(self.peek(), Token::RParen) {
13457 loop {
13458 unit.push(self.parse_expr(0)?);
13459 match self.peek() {
13460 Token::Comma => {
13461 self.advance();
13462 }
13463 Token::RParen => break,
13464 other => {
13465 return Err(self.err(format!(
13466 "expected ',' or ')' in grouping unit, got {other:?}"
13467 )));
13468 }
13469 }
13470 }
13471 }
13472 self.advance(); // )
13473 units.push(unit);
13474 } else {
13475 units.push(alloc::vec![self.parse_expr(0)?]);
13476 }
13477 match self.peek() {
13478 Token::Comma => {
13479 self.advance();
13480 }
13481 Token::RParen => break,
13482 other => {
13483 return Err(self.err(format!(
13484 "expected ',' or ')' in grouping list, got {other:?}"
13485 )));
13486 }
13487 }
13488 }
13489 self.advance(); // )
13490 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13491 units
13492 .iter()
13493 .zip(unit_sel.iter())
13494 .filter(|(_, keep)| **keep)
13495 .flat_map(|(u, _)| u.iter().cloned())
13496 .collect()
13497 };
13498 let n = units.len();
13499 if is_cube {
13500 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13501 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13502 .collect();
13503 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13504 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13505 }
13506 return Ok((0..=n)
13507 .rev()
13508 .map(|keep| {
13509 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13510 flatten(&sel)
13511 })
13512 .collect());
13513 }
13514 // GROUPING SETS ( item [, item]* )
13515 if is_kw(self.peek(), "grouping")
13516 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13517 {
13518 self.advance(); // GROUPING
13519 self.advance(); // SETS
13520 if !matches!(self.peek(), Token::LParen) {
13521 return Err(self.err(format!(
13522 "expected '(' after GROUPING SETS, got {:?}",
13523 self.peek()
13524 )));
13525 }
13526 self.advance(); // outer (
13527 let mut sets: Vec<Vec<Expr>> = Vec::new();
13528 loop {
13529 if matches!(self.peek(), Token::LParen) {
13530 // A parenthesized key list (or the empty set).
13531 self.advance();
13532 let mut set = Vec::new();
13533 if !matches!(self.peek(), Token::RParen) {
13534 loop {
13535 set.push(self.parse_expr(0)?);
13536 match self.peek() {
13537 Token::Comma => {
13538 self.advance();
13539 }
13540 Token::RParen => break,
13541 other => {
13542 return Err(self.err(format!(
13543 "expected ',' or ')' in grouping set, got {other:?}"
13544 )));
13545 }
13546 }
13547 }
13548 }
13549 self.advance(); // )
13550 sets.push(set);
13551 } else {
13552 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13553 // bare expression.
13554 sets.extend(self.parse_grouping_element()?);
13555 }
13556 match self.peek() {
13557 Token::Comma => {
13558 self.advance();
13559 }
13560 Token::RParen => break,
13561 other => {
13562 return Err(self.err(format!(
13563 "expected ',' or ')' after a grouping set, got {other:?}"
13564 )));
13565 }
13566 }
13567 }
13568 self.advance(); // outer )
13569 return Ok(sets);
13570 }
13571 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13572 }
13573
13574 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13575 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13576 // set evaluates to NULL, at any depth. Previously only a *top-level*
13577 // select item equal to a dropped key was nullified, so a key nested in
13578 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13579 // column and failed to resolve against the set's synthetic schema.
13580 if dropped.iter().any(|d| d == expr) {
13581 *expr = Expr::Literal(Literal::Null);
13582 return;
13583 }
13584 if let Expr::FunctionCall { name, args } = expr
13585 && name.eq_ignore_ascii_case("grouping")
13586 {
13587 let mut mask: i64 = 0;
13588 for a in args.iter() {
13589 mask <<= 1;
13590 if dropped.iter().any(|d| d == a) {
13591 mask |= 1;
13592 }
13593 }
13594 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13595 // literal: a bare integer in a select item is indistinguishable
13596 // from a positional reference once `ORDER BY 1` substitutes the
13597 // item back in, and the round-232 position check then read the
13598 // mask value as an out-of-range position. The cast changes
13599 // nothing semantically (grouping() is integer).
13600 *expr = Expr::Cast {
13601 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13602 target: crate::ast::CastTarget::Int,
13603 };
13604 return;
13605 }
13606 // Generic recursion over the common expression shapes the
13607 // SELECT list uses; anything without child expressions is
13608 // left alone.
13609 match expr {
13610 // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13611 //
13612 // A grouping column is NULL in the OUTPUT of a set that
13613 // drops it, and an aggregate over it still aggregates the
13614 // real values. Measured, over 0,1,2,3:
13615 //
13616 // ```text
13617 // SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13618 // PostgreSQL 18.6 the total row is NULL | 6
13619 // MySQL 9.7.2 the total row is NULL | 6
13620 // SPG 7.39.13 NULL | NULL
13621 // ```
13622 //
13623 // The round that made this walk descend "at any depth" was
13624 // right about `COALESCE(g,'TOTAL')` and wrong about
13625 // `SUM(g)`: it turned the aggregate's own input into a NULL
13626 // literal, so the grand total of a rollup keyed on the
13627 // summed column answered nothing. Wrong on BOTH faces.
13628 //
13629 // `grouping(…)` is settled above, before this, so it keeps
13630 // reading the dropped set.
13631 Expr::FunctionCall { name, args } => {
13632 if is_aggregate_function_name(name) {
13633 return;
13634 }
13635 for a in args {
13636 Self::substitute_grouping_calls(a, dropped);
13637 }
13638 }
13639 Expr::AggregateOrdered { .. } => {}
13640 Expr::Binary { lhs, rhs, .. } => {
13641 Self::substitute_grouping_calls(lhs, dropped);
13642 Self::substitute_grouping_calls(rhs, dropped);
13643 }
13644 Expr::Unary { expr: inner, .. } => {
13645 Self::substitute_grouping_calls(inner, dropped);
13646 }
13647 Expr::Cast { expr: inner, .. } => {
13648 Self::substitute_grouping_calls(inner, dropped);
13649 }
13650 Expr::Case {
13651 operand,
13652 branches,
13653 else_branch,
13654 } => {
13655 if let Some(op) = operand {
13656 Self::substitute_grouping_calls(op, dropped);
13657 }
13658 for (w, t) in branches {
13659 Self::substitute_grouping_calls(w, dropped);
13660 Self::substitute_grouping_calls(t, dropped);
13661 }
13662 if let Some(e) = else_branch {
13663 Self::substitute_grouping_calls(e, dropped);
13664 }
13665 }
13666 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13667 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13668 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13669 // …` is the canonical rollup-total label idiom).
13670 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13671 Expr::Like { expr, pattern, .. } => {
13672 Self::substitute_grouping_calls(expr, dropped);
13673 Self::substitute_grouping_calls(pattern, dropped);
13674 }
13675 Expr::InList { expr, list, .. } => {
13676 Self::substitute_grouping_calls(expr, dropped);
13677 for item in list {
13678 Self::substitute_grouping_calls(item, dropped);
13679 }
13680 }
13681 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13682 Expr::Array(items) => {
13683 for item in items {
13684 Self::substitute_grouping_calls(item, dropped);
13685 }
13686 }
13687 Expr::ArraySubscript { target, index } => {
13688 Self::substitute_grouping_calls(target, dropped);
13689 Self::substitute_grouping_calls(index, dropped);
13690 }
13691 Expr::ArraySlice { target, lo, hi } => {
13692 Self::substitute_grouping_calls(target, dropped);
13693 if let Some(lo) = lo {
13694 Self::substitute_grouping_calls(lo, dropped);
13695 }
13696 if let Some(hi) = hi {
13697 Self::substitute_grouping_calls(hi, dropped);
13698 }
13699 }
13700 Expr::AnyAll { expr, array, .. } => {
13701 Self::substitute_grouping_calls(expr, dropped);
13702 Self::substitute_grouping_calls(array, dropped);
13703 }
13704 _ => {}
13705 }
13706 }
13707
13708 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13709 // v7.40.11 — an UNPARENTHESISED `VALUES` list is a query block
13710 // too, so it can be the PEER of a set operation:
13711 //
13712 // SELECT 1 UNION ALL VALUES (2)
13713 //
13714 // The parenthesised form has been a peer since v7.37 D.20 and
13715 // the CTE-body form since 17.6; these two unbracketed positions
13716 // were the pair nobody wrote a case for. Modern psql builds its
13717 // describe queries with `UNION ALL VALUES`, so every backslash
13718 // command — `\d`, `\dt`, `\di` — failed with a syntax error
13719 // pointing into a query the user did not write.
13720 if matches!(self.peek(), Token::Values) {
13721 self.advance(); // VALUES
13722 return self.parse_values_rows_body();
13723 }
13724 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13725 // group: `( <select chain> )` usable anywhere a query block
13726 // is (head or peer of an outer chain). The group's own
13727 // unions ride the returned SelectStatement; the executor's
13728 // nested-peer recursion runs them.
13729 if matches!(self.peek(), Token::LParen)
13730 && matches!(
13731 self.tokens.get(self.pos + 1),
13732 Some(Token::Select | Token::LParen | Token::Values)
13733 )
13734 {
13735 self.advance(); // (
13736 self.enter_nested()?;
13737 // v7.37 D.20 — a group whose head is a VALUES list:
13738 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13739 // otherwise recurse into a nested SELECT/group head.
13740 let mut head = (if matches!(self.peek(), Token::Values) {
13741 self.advance(); // VALUES
13742 self.parse_values_rows_body()
13743 } else {
13744 self.parse_bare_select()
13745 })
13746 .and_then(|mut h| {
13747 self.parse_setop_chain_into(&mut h)?;
13748 Ok(h)
13749 });
13750 self.nest_depth -= 1;
13751 let mut head = match &mut head {
13752 Ok(h) => core::mem::take(h),
13753 Err(_) => return head,
13754 };
13755 // v7.37.17 (17.6 siblings) — group-internal tail:
13756 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13757 // group head, then wrap the group as a derived table
13758 // (SELECT * FROM (group)) so the outer chain / outer
13759 // tail can't clobber the group's own ordering or limit.
13760 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13761 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13762 if s.eq_ignore_ascii_case("fetch"));
13763 if has_tail {
13764 self.parse_select_tail_into(&mut head)?;
13765 head = SelectStatement {
13766 locking: None,
13767 ctes: Vec::new(),
13768 distinct: false,
13769 distinct_on: Vec::new(),
13770 items: alloc::vec![SelectItem::Wildcard],
13771 from: Some(FromClause {
13772 primary: TableRef {
13773 name: "subquery".to_string(),
13774 alias: None,
13775 only: false,
13776 as_of_segment: None,
13777 unnest_expr: None,
13778 unnest_column_aliases: Vec::new(),
13779 with_ordinality: false,
13780 generate_series_args: None,
13781 lateral_subquery: Some(Box::new(head)),
13782 jsonb_each_text_arg: None,
13783 table_fn_call: None,
13784 rows_from: None,
13785 json_table: None,
13786 scalar_fn_item: false,
13787 },
13788 joins: Vec::new(),
13789 }),
13790 where_: None,
13791 group_by: None,
13792 group_by_all: false,
13793 having: None,
13794 unions: Vec::new(),
13795 order_by: Vec::new(),
13796 limit: None,
13797 offset: None,
13798 limit_with_ties: false,
13799 window_check_exprs: Vec::new(),
13800 };
13801 }
13802 if !matches!(self.peek(), Token::RParen) {
13803 return Err(self.err(format!(
13804 "expected ')' after parenthesized query group, got {:?}",
13805 self.peek()
13806 )));
13807 }
13808 self.advance();
13809 return Ok(head);
13810 }
13811 // `TABLE name` shorthand as a query block — valid anywhere
13812 // a SELECT head is (set-op peers included).
13813 if matches!(self.peek(), Token::Table)
13814 && matches!(
13815 self.tokens.get(self.pos + 1),
13816 Some(Token::Ident(_) | Token::QuotedIdent(_))
13817 )
13818 {
13819 return self.parse_table_shorthand();
13820 }
13821 if !matches!(self.peek(), Token::Select) {
13822 return Err(self.err(format!(
13823 "expected SELECT to start a query block, got {:?}",
13824 self.peek()
13825 )));
13826 }
13827 self.advance();
13828 // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13829 //
13830 // It sits where `DISTINCT` sits and tells the optimiser to join
13831 // in the written order. SPG plans its own joins, so the hint is
13832 // accepted and not acted on — but it has to PARSE, because as a
13833 // bare identifier it became a column: measured on the published
13834 // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13835 // column 'straight_join' in 'field list'` where MySQL 9.7.2
13836 // returns the rows. Only in this position, which is the only one
13837 // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13838 if self.mysql_dialect
13839 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13840 {
13841 self.advance();
13842 }
13843 let distinct = if matches!(self.peek(), Token::Distinct) {
13844 self.advance();
13845 true
13846 } else {
13847 false
13848 };
13849 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13850 // keep the first row (per ORDER BY) of each group the
13851 // expressions define. Django's .distinct('field') shape.
13852 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13853 self.advance(); // ON
13854 if !matches!(self.peek(), Token::LParen) {
13855 return Err(self.err(format!(
13856 "expected '(' after DISTINCT ON, got {:?}",
13857 self.peek()
13858 )));
13859 }
13860 self.advance();
13861 let mut exprs = Vec::new();
13862 loop {
13863 exprs.push(self.parse_expr(0)?);
13864 match self.peek() {
13865 Token::Comma => {
13866 self.advance();
13867 }
13868 Token::RParen => break,
13869 other => {
13870 return Err(self.err(format!(
13871 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13872 )));
13873 }
13874 }
13875 }
13876 self.advance(); // )
13877 exprs
13878 } else {
13879 Vec::new()
13880 };
13881 let mut items = self.parse_select_list()?;
13882 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13883 // of CTAS. It sits exactly here in PG's grammar, right after the
13884 // target list.
13885 //
13886 // A comment in `ast.rs` has said since v7.38 that CTAS and
13887 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13888 // `SELECT i INTO t FROM src` answered `syntax error at or near
13889 // "INTO"`, which the differential found while measuring what
13890 // PostgreSQL tags each of the five materialising forms with. A
13891 // comment describing a capability the code does not have is the
13892 // defect this version has been finding all day, and this is the
13893 // one it found in the parser.
13894 //
13895 // `INTO` is captured rather than consumed here: the name has to
13896 // travel out of a function that returns a `SelectStatement`, and
13897 // the caller lowers the whole thing to the CTAS node.
13898 if matches!(self.peek(), Token::Into) {
13899 self.advance();
13900 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13901 // the target, not part of its name. SPG has one storage
13902 // class, so `UNLOGGED` is accepted and means nothing, which
13903 // is what it already means on `CREATE TABLE`.
13904 let mut temporary = false;
13905 loop {
13906 match self.peek().clone() {
13907 Token::Ident(w) | Token::QuotedIdent(w)
13908 if w.eq_ignore_ascii_case("temp")
13909 || w.eq_ignore_ascii_case("temporary") =>
13910 {
13911 temporary = true;
13912 self.advance();
13913 }
13914 Token::Ident(w) | Token::QuotedIdent(w)
13915 if w.eq_ignore_ascii_case("unlogged") =>
13916 {
13917 self.advance();
13918 }
13919 Token::Table => {
13920 self.advance();
13921 }
13922 _ => break,
13923 }
13924 }
13925 let name = match self.peek().clone() {
13926 Token::Ident(w) | Token::QuotedIdent(w) => {
13927 self.advance();
13928 w
13929 }
13930 other => {
13931 return Err(self.err(alloc::format!(
13932 "expected a table name after SELECT … INTO, got {other:?}"
13933 )));
13934 }
13935 };
13936 self.pending_select_into = Some((name, temporary));
13937 }
13938 // Scope the TABLESAMPLE lowering channel to this SELECT:
13939 // stash whatever an enclosing select accumulated, collect
13940 // our own FROM's predicates, restore after the combine.
13941 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13942 let mut from = if matches!(self.peek(), Token::From) {
13943 self.advance();
13944 Some(self.parse_from_clause()?)
13945 } else {
13946 None
13947 };
13948 // v7.37 D.22 — a set-returning function in the projection with no FROM
13949 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13950 // rows. Move the first SRF projection item to a FROM-position derived
13951 // table and replace it in the projection with a reference to its output
13952 // column; sibling scalar columns repeat per SRF row. PG names the output
13953 // column after the function (or its AS alias). Reuses the FROM-SRF
13954 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13955 // works via the targetlist-SRF path.
13956 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13957 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13958 // is exactly what the function's own row shape already is. Anywhere else
13959 // (per outer row, or beside other items) it would need a real record-typed
13960 // projection, so it says so rather than answering something else.
13961 if let [
13962 SelectItem::Expr {
13963 expr: Expr::FunctionCall { name, args },
13964 ..
13965 },
13966 ] = items.as_slice()
13967 && name == "__record_expand"
13968 {
13969 let Some(Expr::FunctionCall {
13970 name: inner_name,
13971 args: inner_args,
13972 }) = args.first()
13973 else {
13974 return Err(self.err(
13975 "(<expr>).* expands a function's record — it needs a function call".into(),
13976 ));
13977 };
13978 if from.is_some() {
13979 return Err(self.err(
13980 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13981 .into(),
13982 ));
13983 }
13984 let fn_ref = TableRef {
13985 name: inner_name.clone(),
13986 alias: None,
13987 only: false,
13988 as_of_segment: None,
13989 unnest_expr: None,
13990 unnest_column_aliases: Vec::new(),
13991 with_ordinality: false,
13992 generate_series_args: None,
13993 lateral_subquery: None,
13994 jsonb_each_text_arg: None,
13995 table_fn_call: Some(Box::new((
13996 inner_name.to_ascii_lowercase(),
13997 inner_args.clone(),
13998 ))),
13999 rows_from: None,
14000 json_table: None,
14001 scalar_fn_item: false,
14002 };
14003 items = alloc::vec![SelectItem::Wildcard];
14004 from = Some(FromClause {
14005 primary: fn_ref,
14006 joins: Vec::new(),
14007 });
14008 }
14009 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
14010 // FROM, keeps its marker: the ENGINE lowers it, because naming the
14011 // record's fields takes the catalog. It becomes a LATERAL of the same
14012 // function plus one item per declared column — the machinery rounds 65
14013 // and 69 already built.
14014 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
14015 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
14016 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
14017 // express, since the lifted one becomes a scan and the other would
14018 // expand per its rows (a cross product, not a zip). So when the
14019 // projection holds more than one top-level function call, the lift steps
14020 // aside and the engine's target-list expansion takes the whole list.
14021 let fn_call_items = items
14022 .iter()
14023 .filter(|it| {
14024 matches!(
14025 it,
14026 SelectItem::Expr {
14027 expr: Expr::FunctionCall { .. },
14028 ..
14029 }
14030 )
14031 })
14032 .count();
14033 if from.is_none() && fn_call_items <= 1 {
14034 let mut found: Option<(usize, TableRef, String)> = None;
14035 for (i, item) in items.iter().enumerate() {
14036 if let SelectItem::Expr {
14037 expr: Expr::FunctionCall { name, args },
14038 alias,
14039 } = item
14040 {
14041 let lname = name.to_ascii_lowercase();
14042 let colname = alias.clone().unwrap_or_else(|| lname.clone());
14043 let (unnest, gs) = match lname.as_str() {
14044 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14045 "generate_series" if (2..=3).contains(&args.len()) => {
14046 (None, Some(args.clone()))
14047 }
14048 // v7.38 (read01) — generate_subscripts(arr, dim) in a
14049 // no-FROM projection yields the 1-based subscripts, i.e.
14050 // generate_series(1, array_length(arr, dim)); an invalid
14051 // dimension makes array_length NULL → 0 rows, as in PG.
14052 "generate_subscripts" if args.len() == 2 => (
14053 None,
14054 Some(alloc::vec![
14055 Expr::Literal(Literal::Integer(1)),
14056 Expr::FunctionCall {
14057 name: "array_length".to_string(),
14058 args: args.clone(),
14059 },
14060 ]),
14061 ),
14062 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14063 // in a no-FROM projection unnest their *_to_array form.
14064 "string_to_table" | "regexp_split_to_table" => {
14065 let array_fn = if lname == "string_to_table" {
14066 "string_to_array"
14067 } else {
14068 "regexp_split_to_array"
14069 };
14070 (
14071 Some(Box::new(Expr::FunctionCall {
14072 name: array_fn.to_string(),
14073 args: args.clone(),
14074 })),
14075 None,
14076 )
14077 }
14078 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14079 // a no-FROM projection expand per element. The scalar form
14080 // returns the elements as a TEXT array, so unnest over the
14081 // same call materialises one row each (same rewrite the
14082 // FROM-clause form uses).
14083 "jsonb_array_elements"
14084 | "json_array_elements"
14085 | "jsonb_array_elements_text"
14086 | "json_array_elements_text"
14087 if args.len() == 1 =>
14088 {
14089 (
14090 Some(Box::new(Expr::FunctionCall {
14091 name: lname.clone(),
14092 args: args.clone(),
14093 })),
14094 None,
14095 )
14096 }
14097 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14098 // in a no-FROM projection expands per match (scalar form
14099 // returns the matches as a TEXT array → unnest).
14100 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14101 Some(Box::new(Expr::FunctionCall {
14102 name: lname.clone(),
14103 args: args.clone(),
14104 })),
14105 None,
14106 ),
14107 _ => continue,
14108 };
14109 found = Some((
14110 i,
14111 TableRef {
14112 name: colname.clone(),
14113 alias: Some(colname.clone()),
14114 only: false,
14115 as_of_segment: None,
14116 unnest_expr: unnest,
14117 unnest_column_aliases: alloc::vec![colname.clone()],
14118 with_ordinality: false,
14119 generate_series_args: gs,
14120 lateral_subquery: None,
14121 jsonb_each_text_arg: None,
14122 table_fn_call: None,
14123 rows_from: None,
14124 json_table: None,
14125 scalar_fn_item: false,
14126 },
14127 colname,
14128 ));
14129 break;
14130 }
14131 }
14132 if let Some((idx, tref, colname)) = found {
14133 from = Some(FromClause {
14134 primary: tref,
14135 joins: Vec::new(),
14136 });
14137 items[idx] = SelectItem::Expr {
14138 expr: Expr::Column(ColumnName {
14139 qualifier: None,
14140 name: colname.clone(),
14141 }),
14142 alias: Some(colname),
14143 };
14144 }
14145 }
14146 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14147 let where_ = if matches!(self.peek(), Token::Where) {
14148 self.advance();
14149 Some(self.parse_expr(0)?)
14150 } else {
14151 None
14152 };
14153 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14154 Some(match acc {
14155 Some(w) => Expr::Binary {
14156 lhs: Box::new(pred),
14157 op: crate::ast::BinOp::And,
14158 rhs: Box::new(w),
14159 },
14160 None => pred,
14161 })
14162 });
14163 self.pending_sample_preds = enclosing_sample_preds;
14164 let mut group_by_all = false;
14165 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14166 // share one expansion: `grouping_sets` lists the key subsets
14167 // (first = primary, assigned to stmt.group_by; the rest
14168 // become UNION ALL peers), `grouping_universe` is the full
14169 // key list used to compute each peer's dropped keys.
14170 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14171 let mut grouping_universe: Vec<Expr> = Vec::new();
14172 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14173 // A BOOL, not the key list: this frame is the statement parser's, and
14174 // round 430 measured that a `Vec` local here is enough on its own to
14175 // tip the 512 KiB nesting guard. The keys are recoverable from
14176 // `grouping_universe`, which a rollup fills with exactly them.
14177 let mut mysql_rollup = false;
14178 let group_by = if matches!(self.peek(), Token::Group) {
14179 self.advance();
14180 if !self.peek_is_by() {
14181 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14182 }
14183 self.advance();
14184 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14185 // every non-aggregate SELECT-list item later.
14186 if matches!(self.peek(), Token::All) {
14187 self.advance();
14188 group_by_all = true;
14189 None
14190 } else {
14191 // v7.39 (round 242) — PG's general grouping-element grammar:
14192 // GROUP BY [DISTINCT] element [, element]*, where an element
14193 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14194 // SETS (…) — mixed freely. Each element yields a list of
14195 // key sets; the query's grouping sets are the CARTESIAN
14196 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14197 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14198 // content. ROLLUP/CUBE members may be composite
14199 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14200 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14201 // parser handled only a lone ROLLUP/CUBE/GS as the whole
14202 // clause.
14203 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14204 self.advance();
14205 true
14206 } else {
14207 false
14208 };
14209 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14210 loop {
14211 element_sets.push(self.parse_grouping_element()?);
14212 if matches!(self.peek(), Token::Comma) {
14213 self.advance();
14214 } else {
14215 break;
14216 }
14217 }
14218 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14219 for el in &element_sets {
14220 let mut next: Vec<Vec<Expr>> = Vec::new();
14221 for base in &total {
14222 for set in el {
14223 let mut merged = base.clone();
14224 for k in set {
14225 if !merged.iter().any(|m| m == k) {
14226 merged.push(k.clone());
14227 }
14228 }
14229 next.push(merged);
14230 }
14231 }
14232 total = next;
14233 }
14234 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14235 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14236 // The keys and the aggregates come out identical; the ROW
14237 // ORDER does not, and that is the part a report depends on.
14238 // MySQL interleaves each group's subtotal right after its
14239 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14240 // where the union-of-grouping-sets expansion emits every
14241 // leaf first and then every subtotal. MariaDB REFUSES an
14242 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14243 // order itself — measured on MariaDB 11 and MySQL 9.7, which
14244 // agree on the order and disagree only on whether ORDER BY
14245 // is allowed (MySQL allows it; SPG allows it too, since
14246 // refusing would break the clients that can write it).
14247 if self.mysql_dialect
14248 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14249 && matches!(
14250 self.tokens.get(self.pos + 1),
14251 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14252 )
14253 {
14254 self.advance(); // WITH
14255 self.advance(); // ROLLUP
14256 let keys = total.into_iter().next().unwrap_or_default();
14257 mysql_rollup = true;
14258 // n+1 prefixes, largest first — the same expansion
14259 // `ROLLUP (…)` produces.
14260 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14261 }
14262 if distinct_sets {
14263 let mut seen: Vec<Vec<String>> = Vec::new();
14264 total.retain(|set| {
14265 let mut key: Vec<String> =
14266 set.iter().map(|e| alloc::format!("{e}")).collect();
14267 key.sort();
14268 if seen.contains(&key) {
14269 false
14270 } else {
14271 seen.push(key);
14272 true
14273 }
14274 });
14275 }
14276 if total.len() > 1 {
14277 let mut universe: Vec<Expr> = Vec::new();
14278 for set in &total {
14279 for k in set {
14280 if !universe.iter().any(|u| u == k) {
14281 universe.push(k.clone());
14282 }
14283 }
14284 }
14285 grouping_universe = universe;
14286 let primary = total[0].clone();
14287 grouping_sets = total;
14288 Some(primary)
14289 } else {
14290 // One set (a plain GROUP BY list, or a single-set
14291 // spelling like GROUPING SETS ((a, b))). An EMPTY
14292 // single set — GROUPING SETS (()) — stays
14293 // `Some(vec![])`: the grand-total group, which must
14294 // run the aggregate path.
14295 Some(total.into_iter().next().unwrap_or_default())
14296 }
14297 }
14298 } else {
14299 None
14300 };
14301 let having = if matches!(self.peek(), Token::Having) {
14302 self.advance();
14303 Some(self.parse_expr(0)?)
14304 } else {
14305 None
14306 };
14307 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14308 // OVER w parsed to a marker above; inline each definition
14309 // into the referencing WindowFunction nodes.
14310 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14311 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14312 self.advance();
14313 loop {
14314 let wname = self.expect_ident_like()?;
14315 if !matches!(self.peek(), Token::As) {
14316 return Err(self.err(format!(
14317 "expected AS after WINDOW {wname}, got {:?}",
14318 self.peek()
14319 )));
14320 }
14321 self.advance();
14322 // v7.39 (round 229) — PG rejects a redefinition outright.
14323 if window_defs
14324 .iter()
14325 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14326 {
14327 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14328 }
14329 let def = self.parse_over_clause()?;
14330 // A definition may itself copy an earlier one
14331 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14332 // so resolve it against the defs already in scope. Same
14333 // copy rules as an `OVER (w1 …)` in the select list.
14334 let mut probe = Expr::WindowFunction {
14335 name: String::new(),
14336 args: Vec::new(),
14337 partition_by: def.0,
14338 order_by: def.1,
14339 frame: def.2,
14340 null_treatment: crate::ast::NullTreatment::Respect,
14341 filter: None,
14342 };
14343 Self::substitute_named_windows(&mut probe, &window_defs)
14344 .map_err(|m| self.err(m))?;
14345 let Expr::WindowFunction {
14346 partition_by,
14347 order_by,
14348 frame,
14349 ..
14350 } = probe
14351 else {
14352 unreachable!("probe is a WindowFunction")
14353 };
14354 window_defs.push((wname, (partition_by, order_by, frame)));
14355 if matches!(self.peek(), Token::Comma) {
14356 self.advance();
14357 continue;
14358 }
14359 break;
14360 }
14361 }
14362 // v7.39 (round 705) — which definitions did anything reference?
14363 // The ones nothing did used to be dropped here, unexamined, so
14364 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14365 // definition whether referenced or not. Their key expressions ride
14366 // out on the statement for the engine to resolve.
14367 let mut window_refs: Vec<String> = Vec::new();
14368 if !window_defs.is_empty() {
14369 for it in &items {
14370 if let SelectItem::Expr { expr, .. } = it {
14371 Self::collect_named_window_refs(expr, &mut window_refs);
14372 }
14373 }
14374 }
14375 let window_check_exprs: Vec<Expr> = window_defs
14376 .iter()
14377 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14378 .flat_map(|(_, (partition, order, _))| {
14379 partition
14380 .iter()
14381 .cloned()
14382 .chain(order.iter().map(|(e, _, _)| e.clone()))
14383 })
14384 .collect();
14385 if !window_defs.is_empty()
14386 || items
14387 .iter()
14388 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14389 {
14390 for it in &mut items {
14391 if let SelectItem::Expr { expr, .. } = it {
14392 Self::substitute_named_windows(expr, &window_defs)
14393 .map_err(|m| self.err(m))?;
14394 }
14395 }
14396 }
14397 // `GROUP BY 1` — positional keys substitute with the Nth
14398 // select item's expression (same contract ORDER BY has had
14399 // since v6.x). Out-of-range positions error.
14400 let group_by = match group_by {
14401 Some(mut keys) => {
14402 for k in &mut keys {
14403 if let Expr::Literal(Literal::Integer(n)) = k {
14404 let idx = *n;
14405 if idx < 1 || idx as usize > items.len() {
14406 return Err(self.err(alloc::format!(
14407 "GROUP BY position {idx} is not in select list"
14408 )));
14409 }
14410 match &items[(idx - 1) as usize] {
14411 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14412 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14413 return Err(self.err(alloc::format!(
14414 "GROUP BY position {idx} references a wildcard item"
14415 )));
14416 }
14417 }
14418 }
14419 }
14420 Some(keys)
14421 }
14422 None => None,
14423 };
14424 let mut stmt = SelectStatement {
14425 locking: None,
14426 ctes: Vec::new(),
14427 distinct,
14428 distinct_on,
14429 items,
14430 from,
14431 where_,
14432 group_by,
14433 group_by_all,
14434 having,
14435 unions: Vec::new(),
14436 order_by: Vec::new(),
14437 limit: None,
14438 offset: None,
14439 limit_with_ties: false,
14440 window_check_exprs,
14441 };
14442 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14443 // first set is the primary (already on stmt.group_by); each
14444 // further set becomes a UNION ALL peer with its dropped
14445 // keys (universe minus the set) replaced by NULL literals
14446 // in the peer's items and group_by. PG-legal: non-grouped
14447 // select items must be group keys or aggregates, so a
14448 // dropped key's occurrences in the projection are exactly
14449 // the ones to nullify.
14450 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14451 // over a plain GROUP BY (every argument must be a group key; the
14452 // mask is then 0) and rejects anything else with 42803. SPG's
14453 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14454 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14455 // function `grouping`".
14456 if grouping_sets.len() <= 1 {
14457 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14458 let mut calls: Vec<Expr> = Vec::new();
14459 for item in &stmt.items {
14460 if let SelectItem::Expr { expr, .. } = item {
14461 Self::collect_grouping_calls(expr, &mut calls);
14462 }
14463 }
14464 if let Some(h) = &stmt.having {
14465 Self::collect_grouping_calls(h, &mut calls);
14466 }
14467 for call in &calls {
14468 let Expr::FunctionCall { args, .. } = call else {
14469 continue;
14470 };
14471 for a in args {
14472 if !keys.iter().any(|k| k == a) {
14473 return Err(self.err(
14474 "arguments to GROUPING must be grouping expressions of the associated query level"
14475 .to_string(),
14476 ));
14477 }
14478 }
14479 }
14480 if !calls.is_empty() {
14481 for item in &mut stmt.items {
14482 if let SelectItem::Expr { expr, .. } = item {
14483 Self::substitute_grouping_calls(expr, &[]);
14484 }
14485 }
14486 if let Some(h) = &mut stmt.having {
14487 Self::substitute_grouping_calls(h, &[]);
14488 }
14489 }
14490 }
14491 if grouping_sets.len() > 1 {
14492 // The primary set's own dropped keys nullify in the
14493 // HEAD's projection too (GROUPING SETS's first set may
14494 // omit keys other sets use).
14495 let primary = grouping_sets[0].clone();
14496 let head_dropped: Vec<Expr> = grouping_universe
14497 .iter()
14498 .filter(|u| !primary.iter().any(|k| k == *u))
14499 .cloned()
14500 .collect();
14501 for set in grouping_sets.iter().skip(1) {
14502 let mut peer = stmt.clone();
14503 peer.unions = Vec::new();
14504 let dropped: Vec<&Expr> = grouping_universe
14505 .iter()
14506 .filter(|u| !set.iter().any(|k| k == *u))
14507 .collect();
14508 // Empty set = grand-total group: `Some(vec![])` forces
14509 // the aggregate path (one collapsed row) instead of a
14510 // per-row passthrough. See the primary-set note above.
14511 peer.group_by = Some(set.clone());
14512 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14513 for item in &mut peer.items {
14514 if let SelectItem::Expr { expr, alias } = item {
14515 if dropped.iter().any(|d| *d == expr) {
14516 // v7.39 — keep the dropped key's name on the
14517 // NULL literal so the UNION output column
14518 // (and any top-level ORDER BY on it) still
14519 // resolves.
14520 if alias.is_none()
14521 && let Expr::Column(c) = &expr
14522 {
14523 *alias = Some(c.name.clone());
14524 }
14525 *expr = Expr::Literal(Literal::Null);
14526 } else {
14527 Self::substitute_grouping_calls(expr, &dropped_owned);
14528 }
14529 }
14530 }
14531 if let Some(h) = &mut peer.having {
14532 Self::substitute_grouping_calls(h, &dropped_owned);
14533 }
14534 stmt.unions.push((UnionKind::All, peer));
14535 }
14536 for item in &mut stmt.items {
14537 if let SelectItem::Expr { expr, alias } = item {
14538 if head_dropped.iter().any(|d| d == expr) {
14539 if alias.is_none()
14540 && let Expr::Column(c) = &expr
14541 {
14542 *alias = Some(c.name.clone());
14543 }
14544 *expr = Expr::Literal(Literal::Null);
14545 } else {
14546 Self::substitute_grouping_calls(expr, &head_dropped);
14547 }
14548 }
14549 }
14550 if let Some(h) = &mut stmt.having {
14551 Self::substitute_grouping_calls(h, &head_dropped);
14552 }
14553 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14554 // (while `grouping_universe` / the per-branch sets are in scope). For
14555 // each grouping() call in it, inject a per-branch hidden column
14556 // `__grp_ord_K` carrying that branch's mask into the head + every
14557 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14558 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14559 // from the final output. A standalone grouping-set query has ORDER BY
14560 // (not an explicit set-op) next, so consuming it here is safe.
14561 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14562 // rollup carries the hierarchical order: sort by the grouping
14563 // keys with the rolled-up NULLs last, which is exactly the
14564 // interleaving both oracles emit. A client's own ORDER BY wins,
14565 // which is what MySQL does (MariaDB refuses to let one be
14566 // written at all).
14567 // The synthesised keys have to travel the SAME path a written
14568 // ORDER BY does: the block below is what turns a `grouping()`
14569 // call into the per-branch `__grp_ord_K` column the engine can
14570 // actually sort on. Bypassing it left a bare `grouping(text)`
14571 // for the evaluator to reject.
14572 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14573 self.parse_order_by_keys()?
14574 } else if mysql_rollup {
14575 Self::mysql_rollup_order(&grouping_universe)
14576 } else {
14577 Vec::new()
14578 };
14579 if !synthesised_or_parsed.is_empty() {
14580 let mut order_keys = synthesised_or_parsed;
14581 let mut grp_exprs: Vec<Expr> = Vec::new();
14582 for ob in &order_keys {
14583 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14584 }
14585 for (k, gexpr) in grp_exprs.iter().enumerate() {
14586 let colname = alloc::format!("__grp_ord_{k}");
14587 // Head branch (primary set) uses `head_dropped`.
14588 let mut he = gexpr.clone();
14589 Self::substitute_grouping_calls(&mut he, &head_dropped);
14590 stmt.items.push(SelectItem::Expr {
14591 expr: he,
14592 alias: Some(colname.clone()),
14593 });
14594 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14595 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14596 let set = &grouping_sets[i + 1];
14597 let dropped: Vec<Expr> = grouping_universe
14598 .iter()
14599 .filter(|u| !set.iter().any(|k| k == *u))
14600 .cloned()
14601 .collect();
14602 let mut pe = gexpr.clone();
14603 Self::substitute_grouping_calls(&mut pe, &dropped);
14604 peer.items.push(SelectItem::Expr {
14605 expr: pe,
14606 alias: Some(colname.clone()),
14607 });
14608 }
14609 }
14610 for ob in &mut order_keys {
14611 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14612 }
14613 // v7.40.0 — and a KEY the order sorts on that the query
14614 // did not project.
14615 //
14616 // A UNION's ORDER BY can only name output columns, so
14617 // the rollup order synthesised over `grouping_universe`
14618 // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14619 // ROLLUP` and the query answered `column "qty" does not
14620 // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14621 // by the key whether or not it is selected. The key
14622 // travels as a hidden column, exactly as the grouping
14623 // mask above does, and is stripped from the output by
14624 // the same rule.
14625 let mut key_exprs: Vec<Expr> = Vec::new();
14626 for ob in &order_keys {
14627 let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14628 let projected = stmt
14629 .items
14630 .iter()
14631 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14632 if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14633 key_exprs.push(ob.expr.clone());
14634 }
14635 }
14636 for (k, kexpr) in key_exprs.iter().enumerate() {
14637 let colname = alloc::format!("__grp_key_{k}");
14638 let mut he = kexpr.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 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14645 let set = &grouping_sets[i + 1];
14646 let dropped: Vec<Expr> = grouping_universe
14647 .iter()
14648 .filter(|u| !set.iter().any(|kk| kk == *u))
14649 .cloned()
14650 .collect();
14651 let mut pe = kexpr.clone();
14652 Self::substitute_grouping_calls(&mut pe, &dropped);
14653 peer.items.push(SelectItem::Expr {
14654 expr: pe,
14655 alias: Some(colname.clone()),
14656 });
14657 }
14658 for ob in &mut order_keys {
14659 if &ob.expr == kexpr {
14660 ob.expr = Expr::Column(crate::ast::ColumnName {
14661 name: colname.clone(),
14662 qualifier: None,
14663 });
14664 }
14665 }
14666 }
14667 stmt.order_by = order_keys;
14668 }
14669 }
14670 Ok(stmt)
14671 }
14672
14673 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14674 /// as ORDER BY keys.
14675 ///
14676 /// Per key: the rollup marker, then the key. Sorting on the key alone
14677 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14678 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14679 /// the ROLLUP-introduced NULL last, and both print as NULL.
14680 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14681 /// real group including the data-NULL one, 1 only for the row the
14682 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14683 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14684 ///
14685 /// `#[inline(never)]`: its locals must not join the statement parser's
14686 /// frame, which round 430 measured sitting against the nesting guard.
14687 #[inline(never)]
14688 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14689 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14690 for e in keys {
14691 out.push(OrderBy {
14692 expr: Expr::FunctionCall {
14693 name: "grouping".into(),
14694 args: alloc::vec![e.clone()],
14695 },
14696 desc: false,
14697 nulls_first: None,
14698 collation: None,
14699 });
14700 out.push(OrderBy {
14701 expr: e.clone(),
14702 desc: false,
14703 // MySQL orders NULL first on an ascending key.
14704 nulls_first: Some(true),
14705 collation: None,
14706 });
14707 }
14708 out
14709 }
14710
14711 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14712 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14713 #[inline(never)]
14714 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14715 use crate::ast::MaintainKind;
14716 self.skip_paren_option_list();
14717 let kind = match self.peek() {
14718 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14719 Token::Table | Token::Index => {
14720 self.advance();
14721 MaintainKind::ReindexRelation
14722 }
14723 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14724 "index" | "table" => {
14725 self.advance();
14726 MaintainKind::ReindexRelation
14727 }
14728 "schema" => {
14729 self.advance();
14730 MaintainKind::ReindexSchema
14731 }
14732 "system" | "database" => {
14733 self.advance();
14734 MaintainKind::Whole
14735 }
14736 // PG requires the object type; anything else is the
14737 // caller's problem, not something to swallow.
14738 _ => MaintainKind::ReindexRelation,
14739 },
14740 _ => MaintainKind::Whole,
14741 };
14742 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14743 // allows the plain form, so the modifier is recorded rather than
14744 // skipped. It still has no effect on how the reindex runs.
14745 let mut concurrently = false;
14746 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14747 self.advance();
14748 concurrently = true;
14749 }
14750 let target = self.take_optional_maintain_name();
14751 self.consume_until_statement_boundary();
14752 Ok(Statement::Maintain {
14753 kind,
14754 concurrently,
14755 target,
14756 })
14757 }
14758
14759 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14760 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14761 #[inline(never)]
14762 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14763 use crate::ast::MaintainKind;
14764 self.skip_paren_option_list();
14765 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14766 self.advance();
14767 }
14768 let target = self.take_optional_maintain_name();
14769 self.consume_until_statement_boundary();
14770 Ok(Statement::Maintain {
14771 kind: if target.is_some() {
14772 MaintainKind::ClusterRelation
14773 } else {
14774 MaintainKind::Whole
14775 },
14776 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14777 // transaction block quite happily (measured).
14778 concurrently: false,
14779 target,
14780 })
14781 }
14782
14783 /// The next token as a relation / schema name, when there is one.
14784 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14785 match self.peek() {
14786 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14787 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14788 _ => None,
14789 },
14790 _ => None,
14791 }
14792 }
14793
14794 /// A parenthesised option list, absorbed.
14795 fn skip_paren_option_list(&mut self) {
14796 if !matches!(self.peek(), Token::LParen) {
14797 return;
14798 }
14799 let mut depth = 0usize;
14800 loop {
14801 match self.advance() {
14802 Token::LParen => depth += 1,
14803 Token::RParen => {
14804 depth -= 1;
14805 if depth == 0 {
14806 return;
14807 }
14808 }
14809 Token::Eof => return,
14810 _ => {}
14811 }
14812 }
14813 }
14814
14815 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14816 /// column list.
14817 ///
14818 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14819 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14820 /// / ALL. The three that describe physical storage have no meaning
14821 /// here, so they parse and change nothing rather than making a
14822 /// dump that mentions them fail to load.
14823 ///
14824 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14825 /// parse chain the nesting sentinel is tuned against.
14826 #[inline(never)]
14827 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14828 self.advance(); // LIKE
14829 let source = self.expect_ident_like()?;
14830 let mut options = crate::ast::LikeOptions::default();
14831 loop {
14832 let including = match self.peek() {
14833 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14834 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14835 _ => break,
14836 };
14837 self.advance();
14838 // `ALL` lexes as its own keyword, not an identifier.
14839 let opt = if matches!(self.peek(), Token::All) {
14840 self.advance();
14841 alloc::string::String::from("all")
14842 } else {
14843 self.expect_ident_like()?
14844 };
14845 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14846 o.defaults = on;
14847 o.constraints = on;
14848 o.identity = on;
14849 o.generated = on;
14850 o.indexes = on;
14851 o.comments = on;
14852 };
14853 match opt.to_ascii_lowercase().as_str() {
14854 "all" => set(&mut options, including),
14855 "defaults" => options.defaults = including,
14856 "constraints" => options.constraints = including,
14857 "identity" => options.identity = including,
14858 "generated" => options.generated = including,
14859 "indexes" => options.indexes = including,
14860 "comments" => options.comments = including,
14861 // No storage model to copy into.
14862 "storage" | "statistics" | "compression" => {}
14863 other => {
14864 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14865 }
14866 }
14867 }
14868 Ok(crate::ast::LikeSpec {
14869 source,
14870 at,
14871 options,
14872 keep_index_names: false,
14873 })
14874 }
14875
14876 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14877 // Caller already consumed CREATE; we're sitting on TABLE.
14878 debug_assert!(matches!(self.peek(), Token::Table));
14879 self.advance();
14880 let if_not_exists = self.consume_if_not_exists();
14881 let name = self.expect_ident_like()?;
14882 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14883 // child shape has no column list; the child inherits its
14884 // columns from the parent at engine-DDL time. Detect it
14885 // before the `(` requirement below.
14886 if matches!(self.peek(), Token::Partition)
14887 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14888 {
14889 self.advance(); // PARTITION
14890 self.advance(); // of
14891 let partition_of = self.parse_partition_of_tail()?;
14892 return Ok(Statement::CreateTable(CreateTableStatement {
14893 temporary: false,
14894 name,
14895 engine: None,
14896 auto_increment: None,
14897 columns: Vec::new(),
14898 like_specs: Vec::new(),
14899 inherits: Vec::new(),
14900 if_not_exists,
14901 foreign_keys: Vec::new(),
14902 table_constraints: Vec::new(),
14903 partition_by: None,
14904 partition_of: Some(partition_of),
14905 }));
14906 }
14907 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14908 // the materialized-view materialisation path (run the SELECT, infer the
14909 // column types, create + populate the table) but marks the node so the
14910 // executor creates a plain table without a mat-view registry entry.
14911 if matches!(self.peek(), Token::As) {
14912 self.advance();
14913 let body_stmt = self.parse_select_stmt()?;
14914 let Statement::Select(body) = body_stmt else {
14915 return Err(self.err(format!(
14916 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14917 )));
14918 };
14919 let with_data = self.parse_optional_with_data(true)?;
14920 return Ok(Statement::CreateMaterializedView(
14921 crate::ast::CreateMaterializedViewStatement {
14922 temporary: false,
14923 name,
14924 if_not_exists,
14925 columns: Vec::new(),
14926 body,
14927 with_data,
14928 as_plain_table: true,
14929 },
14930 ));
14931 }
14932 // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14933 // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14934 // written without the parentheses. It was a syntax error, so a
14935 // schema written against MySQL could not be loaded at all.
14936 //
14937 // Measured on MySQL 9.7.2: the copy takes the columns, their
14938 // defaults and the indexes, and takes neither the rows nor the
14939 // foreign keys — which is exactly `INCLUDING ALL` here, since
14940 // SPG's LIKE has never copied foreign keys.
14941 if matches!(self.peek(), Token::Like) {
14942 let at = self.pos;
14943 let spec = self.parse_create_table_like(at)?;
14944 let spec = crate::ast::LikeSpec {
14945 options: crate::ast::LikeOptions {
14946 defaults: true,
14947 constraints: true,
14948 identity: true,
14949 generated: true,
14950 indexes: true,
14951 comments: true,
14952 },
14953 keep_index_names: true,
14954 ..spec
14955 };
14956 return Ok(Statement::CreateTable(CreateTableStatement {
14957 temporary: false,
14958 name,
14959 engine: None,
14960 auto_increment: None,
14961 columns: Vec::new(),
14962 like_specs: alloc::vec![spec],
14963 inherits: Vec::new(),
14964 if_not_exists,
14965 foreign_keys: Vec::new(),
14966 table_constraints: Vec::new(),
14967 partition_by: None,
14968 partition_of: None,
14969 }));
14970 }
14971 if !matches!(self.peek(), Token::LParen) {
14972 return Err(self.err(format!(
14973 "expected '(' after table name, got {:?}",
14974 self.peek()
14975 )));
14976 }
14977 self.advance();
14978 let mut columns = Vec::new();
14979 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14980 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14981 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14982 loop {
14983 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14984 // column list. It is how a child that adds nothing of its own is
14985 // written, and this loop demanded at least one entry: `syntax
14986 // error at or near ")"`. The child takes the parent's columns,
14987 // which the INHERITS clause already arranges.
14988 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14989 self.advance();
14990 break;
14991 }
14992 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14993 // clauses from column definitions. Constraints start
14994 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14995 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14996 // a column.
14997 if self.peek_table_level_pk_start() {
14998 table_constraints.push(self.parse_table_level_primary_key()?);
14999 } else if matches!(self.peek(), Token::Like) {
15000 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
15001 // <opt> ]*`. The source table's shape lives in the catalog,
15002 // so this records the clause and the engine expands it.
15003 like_specs.push(self.parse_create_table_like(columns.len())?);
15004 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
15005 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
15006 table_constraints.push(self.parse_table_level_exclude()?);
15007 } else if self.peek_table_level_unique_start() {
15008 table_constraints.push(self.parse_table_level_unique()?);
15009 } else if self.peek_table_level_check_start() {
15010 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
15011 table_constraints.push(self.parse_table_level_check()?);
15012 } else if self.peek_mysql_inline_key_start() {
15013 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
15014 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
15015 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
15016 // inside the column list. Skip name + paren list;
15017 // for UNIQUE KEY, register as a UC.
15018 if let Some(uc) = self.parse_mysql_inline_key()? {
15019 table_constraints.push(uc);
15020 }
15021 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
15022 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
15023 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
15024 // CHECK is named, and the named-CONSTRAINT arm used
15025 // to accept FOREIGN KEY only. The name is accepted
15026 // and discarded — same handling as every other SPG
15027 // constraint name.
15028 self.advance(); // CONSTRAINT
15029 // v7.39 (read01 round 48) — the name is kept now: the schema
15030 // stores it, so DROP / RENAME CONSTRAINT can find it.
15031 let con_name = self.expect_ident_like()?;
15032 let mut tc = match kind {
15033 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
15034 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
15035 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
15036 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
15037 };
15038 match &mut tc {
15039 crate::ast::TableConstraint::Check { name, .. }
15040 | crate::ast::TableConstraint::Unique { name, .. }
15041 | crate::ast::TableConstraint::PrimaryKey { name, .. }
15042 | crate::ast::TableConstraint::Exclude { name, .. } => {
15043 *name = Some(con_name);
15044 }
15045 _ => {}
15046 }
15047 table_constraints.push(tc);
15048 } else if self.peek_constraint_or_fk_start() {
15049 foreign_keys.push(self.parse_table_level_fk()?);
15050 } else {
15051 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15052 // v7.13.0 — fold inline UNIQUE / CHECK column
15053 // constraints into table-level entries so the
15054 // engine path stays uniform.
15055 if col.is_unique {
15056 table_constraints.push(crate::ast::TableConstraint::Unique {
15057 name: None,
15058 columns: alloc::vec![col.name.clone()],
15059 nulls_not_distinct: col.unique_nulls_not_distinct,
15060 deferrable: col.constraint_deferrable,
15061 initially_deferred: col.constraint_initially_deferred,
15062 prefix_lengths: Vec::new(),
15063 });
15064 }
15065 if let Some(check_expr) = col.check.clone() {
15066 table_constraints.push(crate::ast::TableConstraint::Check {
15067 name: None,
15068 expr: check_expr,
15069 not_valid: false,
15070 });
15071 }
15072 columns.push(col);
15073 if let Some(fk) = col_level_fk {
15074 foreign_keys.push(fk);
15075 }
15076 }
15077 match self.peek() {
15078 Token::Comma => {
15079 self.advance();
15080 }
15081 Token::RParen => {
15082 self.advance();
15083 break;
15084 }
15085 other => {
15086 return Err(
15087 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15088 );
15089 }
15090 }
15091 }
15092 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15093 // `CREATE TABLE k (LIKE t)` is a complete definition even though
15094 // nothing is written between the parentheses.
15095 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15096 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15097 // empty parentheses were a parse error in their own right — quite apart
15098 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15099 // SPG does not have (filed separately).
15100 let _ = &like_specs;
15101 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15102 // It sits between the column list and the MySQL table options,
15103 // and it was a syntax error until this round.
15104 let mut inherits: Vec<String> = Vec::new();
15105 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15106 if k.eq_ignore_ascii_case("inherits"))
15107 {
15108 self.advance();
15109 if !matches!(self.peek(), Token::LParen) {
15110 return Err(self.err(alloc::format!(
15111 "expected ( after INHERITS, got {:?}",
15112 self.peek()
15113 )));
15114 }
15115 self.advance();
15116 loop {
15117 inherits.push(self.expect_ident_like()?);
15118 if matches!(self.peek(), Token::Comma) {
15119 self.advance();
15120 continue;
15121 }
15122 break;
15123 }
15124 if !matches!(self.peek(), Token::RParen) {
15125 return Err(self.err(alloc::format!(
15126 "expected ) closing INHERITS, got {:?}",
15127 self.peek()
15128 )));
15129 }
15130 self.advance();
15131 }
15132 // v7.14.0 — consume MySQL/MariaDB table options after the
15133 // closing `)`. mysqldump emits things like
15134 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15135 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15136 // SPG accepts all forms as no-ops (each option is
15137 // `<ident> [=] <ident-or-string>` separated by whitespace).
15138 let (engine, auto_increment) = self.consume_mysql_table_options();
15139 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15140 // SPG has no per-table reloptions, so accept and ignore them so a
15141 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15142 self.consume_with_reloptions();
15143 // v7.37.6-B — declarative-partition-parent suffix
15144 // (`PARTITION BY RANGE (key_col)`) sits after the column
15145 // list + MySQL table-options. v7.37.6-B only accepts RANGE
15146 // and locks the key column at one ident; the engine then
15147 // verifies the column type is TIMESTAMPTZ.
15148 let partition_by = if matches!(self.peek(), Token::Partition) {
15149 self.advance(); // PARTITION
15150 if !self.peek_is_by() {
15151 return Err(self.err(format!(
15152 "expected BY after PARTITION, got {:?}",
15153 self.peek()
15154 )));
15155 }
15156 self.advance();
15157 Some(self.parse_partition_by_tail()?)
15158 } else {
15159 None
15160 };
15161 Ok(Statement::CreateTable(CreateTableStatement {
15162 temporary: false,
15163 name,
15164 engine,
15165 auto_increment,
15166 columns,
15167 like_specs,
15168 inherits,
15169 if_not_exists,
15170 foreign_keys,
15171 table_constraints,
15172 partition_by,
15173 partition_of: None,
15174 }))
15175 }
15176
15177 /// v7.37.6-B — case-insensitive ident match helper for the
15178 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15179 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15180 /// didn't burn a global keyword slot for each (see the
15181 /// `Token::Partition` doc-comment in `lexer.rs`).
15182 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15183 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15184 }
15185
15186 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15187 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15188 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15189 use crate::ast::{PartitionBySpec, PartitionKindAst};
15190 let kind = match self.peek() {
15191 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15192 self.advance();
15193 PartitionKindAst::Range
15194 }
15195 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15196 self.advance();
15197 PartitionKindAst::List
15198 }
15199 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15200 self.advance();
15201 PartitionKindAst::Hash
15202 }
15203 other => {
15204 return Err(self.err(format!(
15205 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15206 )));
15207 }
15208 };
15209 if !matches!(self.peek(), Token::LParen) {
15210 return Err(self.err(format!(
15211 "expected '(' after PARTITION BY <strategy>, got {:?}",
15212 self.peek()
15213 )));
15214 }
15215 self.advance();
15216 let mut key_columns = Vec::new();
15217 loop {
15218 key_columns.push(self.expect_ident_like()?);
15219 match self.peek() {
15220 Token::Comma => {
15221 self.advance();
15222 }
15223 Token::RParen => {
15224 self.advance();
15225 break;
15226 }
15227 other => {
15228 return Err(self.err(format!(
15229 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15230 )));
15231 }
15232 }
15233 }
15234 if key_columns.is_empty() {
15235 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15236 }
15237 Ok(PartitionBySpec { kind, key_columns })
15238 }
15239
15240 /// v7.37.6-B — after `PARTITION OF`, expect
15241 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15242 /// or
15243 /// <parent> DEFAULT
15244 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15245 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15246 let parent_name = self.expect_ident_like()?;
15247 // v7.37.6-B rejects an explicit column list — the child
15248 // inherits from the parent. mailrs round-7 taught us that
15249 // CREATE TABLE-side schema reconciliation hides drift, so
15250 // we surface this as a parse error rather than silently
15251 // ignoring user columns.
15252 if matches!(self.peek(), Token::LParen) {
15253 return Err(self.err(
15254 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15255 at v7.37.6-B; the child inherits its columns from the parent"
15256 .to_string(),
15257 ));
15258 }
15259 let bounds = match self.peek() {
15260 Token::Default => {
15261 self.advance();
15262 PartitionOfBoundsAst::Default
15263 }
15264 Token::For => {
15265 self.advance();
15266 if !matches!(self.peek(), Token::Values) {
15267 return Err(
15268 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15269 );
15270 }
15271 self.advance();
15272 // WITH is not a reserved Token in the lexer — it lexes
15273 // as Token::Ident("with"). Disambiguate manually.
15274 let want_with = matches!(
15275 self.peek(),
15276 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15277 );
15278 if want_with {
15279 self.advance();
15280 if !matches!(self.peek(), Token::LParen) {
15281 return Err(self.err(format!(
15282 "expected '(' after FOR VALUES WITH, got {:?}",
15283 self.peek()
15284 )));
15285 }
15286 self.advance();
15287 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15288 loop {
15289 let key = self.expect_ident_like()?;
15290 let n = match self.peek().clone() {
15291 Token::Integer(v) if u32::try_from(v).is_ok() => {
15292 self.advance();
15293 v as u32
15294 }
15295 other => {
15296 return Err(self.err(format!(
15297 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15298 )));
15299 }
15300 };
15301 match key.to_ascii_uppercase().as_str() {
15302 "MODULUS" => modulus = Some(n),
15303 "REMAINDER" => remainder = Some(n),
15304 other => {
15305 return Err(self.err(format!(
15306 "FOR VALUES WITH: unknown key {other:?}; \
15307 expected MODULUS or REMAINDER"
15308 )));
15309 }
15310 }
15311 match self.peek() {
15312 Token::Comma => {
15313 self.advance();
15314 }
15315 Token::RParen => {
15316 self.advance();
15317 break;
15318 }
15319 other => {
15320 return Err(self.err(format!(
15321 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15322 )));
15323 }
15324 }
15325 }
15326 let modulus = modulus
15327 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15328 let remainder = remainder.ok_or_else(|| {
15329 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15330 })?;
15331 if modulus == 0 {
15332 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15333 }
15334 if remainder >= modulus {
15335 return Err(self.err(format!(
15336 "FOR VALUES WITH: REMAINDER ({remainder}) \
15337 must be < MODULUS ({modulus})"
15338 )));
15339 }
15340 PartitionOfBoundsAst::Hash { modulus, remainder }
15341 } else {
15342 match self.peek() {
15343 Token::From => {
15344 self.advance();
15345 let lower = Box::new(self.parse_partition_bound_expr()?);
15346 if !matches!(self.peek(), Token::To) {
15347 return Err(self.err(format!(
15348 "expected TO after FROM (...), got {:?}",
15349 self.peek()
15350 )));
15351 }
15352 self.advance();
15353 let upper = Box::new(self.parse_partition_bound_expr()?);
15354 PartitionOfBoundsAst::Range { lower, upper }
15355 }
15356 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15357 Token::In => {
15358 self.advance();
15359 if !matches!(self.peek(), Token::LParen) {
15360 return Err(self.err(format!(
15361 "expected '(' after FOR VALUES IN, got {:?}",
15362 self.peek()
15363 )));
15364 }
15365 self.advance();
15366 let mut values = Vec::new();
15367 loop {
15368 values.push(self.parse_expr(0)?);
15369 match self.peek() {
15370 Token::Comma => {
15371 self.advance();
15372 }
15373 Token::RParen => {
15374 self.advance();
15375 break;
15376 }
15377 other => {
15378 return Err(self.err(format!(
15379 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15380 )));
15381 }
15382 }
15383 }
15384 if values.is_empty() {
15385 return Err(self.err(
15386 "FOR VALUES IN requires at least one literal".to_string(),
15387 ));
15388 }
15389 PartitionOfBoundsAst::List { values }
15390 }
15391 other => {
15392 return Err(self.err(format!(
15393 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15394 )));
15395 }
15396 }
15397 }
15398 }
15399 other => {
15400 return Err(self.err(format!(
15401 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15402 )));
15403 }
15404 };
15405 Ok(PartitionOfSpec {
15406 parent_name,
15407 bounds,
15408 })
15409 }
15410
15411 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15412 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15413 /// markers (no-arg builtins) so the engine resolves them
15414 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15415 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15416 if !matches!(self.peek(), Token::LParen) {
15417 return Err(self.err(format!(
15418 "expected '(' before partition bound, got {:?}",
15419 self.peek()
15420 )));
15421 }
15422 self.advance();
15423 let expr = match self.peek() {
15424 Token::Ident(s) | Token::QuotedIdent(s)
15425 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15426 {
15427 let name = s.to_ascii_uppercase();
15428 self.advance();
15429 crate::ast::Expr::FunctionCall {
15430 name,
15431 args: Vec::new(),
15432 }
15433 }
15434 _ => self.parse_expr(0)?,
15435 };
15436 if !matches!(self.peek(), Token::RParen) {
15437 return Err(self.err(format!(
15438 "expected ')' after partition bound, got {:?}",
15439 self.peek()
15440 )));
15441 }
15442 self.advance();
15443 Ok(expr)
15444 }
15445
15446 /// v7.14.0 — true when the next tokens look like an inline
15447 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15448 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15449 /// — each followed by an optional name + `(...)`. Critical:
15450 /// a column NAMED `key` / `index` (PG accepts as ident) must
15451 /// NOT be mistaken for the KEY constraint shape. We disambig
15452 /// by requiring the keyword to be followed by either `(` or
15453 /// `<ident> (`.
15454 fn peek_mysql_inline_key_start(&self) -> bool {
15455 let cur = self.peek();
15456 // Shapes:
15457 // KEY (cols)
15458 // KEY name (cols)
15459 // INDEX (cols)
15460 // INDEX name (cols)
15461 // UNIQUE KEY [name] (cols)
15462 // UNIQUE INDEX [name] (cols)
15463 // FULLTEXT [KEY|INDEX] [name] (cols)
15464 // SPATIAL [KEY|INDEX] [name] (cols)
15465 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15466 // tokens at skip = the position AFTER the index-form
15467 // keywords (KEY/INDEX) have been consumed.
15468 match self.tokens.get(skip) {
15469 Some(Token::LParen) => true,
15470 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15471 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15472 }
15473 _ => false,
15474 }
15475 };
15476 // `INDEX` lexes as Token::Index (reserved), not as
15477 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15478 // start; the peek helper below handles either.
15479 let is_key_or_index_tok = |t: &Token| -> bool {
15480 matches!(t, Token::Index)
15481 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15482 };
15483 match cur {
15484 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15485 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15486 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15487 }
15488 Token::Ident(s)
15489 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15490 {
15491 let nxt = self.tokens.get(self.pos + 1);
15492 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15493 self.pos + 2
15494 } else {
15495 self.pos + 1
15496 };
15497 after_keyword_followed_by_paren_or_ident_paren(after_after)
15498 }
15499 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15500 let nxt = self.tokens.get(self.pos + 1);
15501 if !nxt.is_some_and(is_key_or_index_tok) {
15502 return false;
15503 }
15504 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15505 }
15506 _ => false,
15507 }
15508 }
15509
15510 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15511 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15512 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15513 /// returns Some(TableConstraint::Index) so the engine builds
15514 /// a real BTree index on the leading column (mysqldump
15515 /// `KEY idx_posts_author (author_id)` shape).
15516 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15517 /// (the storage layer has no matching AM).
15518 fn parse_mysql_inline_key(
15519 &mut self,
15520 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15521 // Detect UNIQUE prefix.
15522 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15523 {
15524 self.advance();
15525 true
15526 } else {
15527 false
15528 };
15529 // Consume FULLTEXT / SPATIAL prefix and record which one
15530 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15531 // dedicated TableConstraint variant so the engine can
15532 // build a tsvector-GIN; SPATIAL still has no matching
15533 // AM, so it falls back to accept-as-no-op.
15534 let mut is_fulltext = false;
15535 let mut is_spatial = false;
15536 if let Token::Ident(s) = self.peek().clone() {
15537 if s.eq_ignore_ascii_case("fulltext") {
15538 self.advance();
15539 is_fulltext = true;
15540 } else if s.eq_ignore_ascii_case("spatial") {
15541 self.advance();
15542 is_spatial = true;
15543 }
15544 }
15545 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15546 // (reserved); accept either token shape.
15547 match self.peek() {
15548 Token::Index => {
15549 self.advance();
15550 }
15551 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15552 self.advance();
15553 }
15554 other => {
15555 return Err(self.err(alloc::format!(
15556 "expected KEY/INDEX in inline index declaration, got {other:?}"
15557 )));
15558 }
15559 }
15560 // Optional index name (an ident before the `(`).
15561 // v7.15.0 — capture the name when present so the engine
15562 // builds the secondary index under the user's chosen
15563 // name (matches mysqldump's `KEY idx_x (col)` shape).
15564 let mut idx_name: Option<String> = None;
15565 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15566 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15567 {
15568 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15569 idx_name = Some(s);
15570 }
15571 }
15572 // Optional `USING BTREE` / `USING HASH` (MySQL).
15573 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15574 self.advance();
15575 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15576 self.advance();
15577 }
15578 }
15579 // Required column list `(col [, col]*)`.
15580 if !matches!(self.peek(), Token::LParen) {
15581 return Err(self.err(alloc::format!(
15582 "expected '(' in inline KEY/INDEX, got {:?}",
15583 self.peek()
15584 )));
15585 }
15586 self.advance();
15587 let mut cols: Vec<String> = Vec::new();
15588 let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15589 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15590 self.advance();
15591 cols.push(s);
15592 // v7.40.0 — the per-column `(length)` prefix is KEPT.
15593 //
15594 // It used to be skipped, so `KEY kb (b(4))` was accepted and
15595 // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15596 // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15597 // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15598 // then unrecorded is the worst of the three answers.
15599 let mut prefix: Option<u32> = None;
15600 if matches!(self.peek(), Token::LParen) {
15601 let mut depth = 1usize;
15602 self.advance();
15603 if let Token::Integer(n) = self.peek()
15604 && let Ok(v) = u32::try_from(*n)
15605 {
15606 prefix = Some(v);
15607 }
15608 while depth > 0 {
15609 match self.peek() {
15610 Token::LParen => depth += 1,
15611 Token::RParen => depth -= 1,
15612 Token::Eof => break,
15613 _ => {}
15614 }
15615 self.advance();
15616 }
15617 }
15618 prefix_lengths.push(prefix);
15619 // Skip optional ASC / DESC.
15620 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15621 || matches!(self.peek(), Token::Asc | Token::Desc)
15622 {
15623 self.advance();
15624 }
15625 if matches!(self.peek(), Token::Comma) {
15626 self.advance();
15627 continue;
15628 }
15629 break;
15630 }
15631 if matches!(self.peek(), Token::RParen) {
15632 self.advance();
15633 }
15634 // Trailing options on the inline index — comment / etc.
15635 // Skip until comma or `)`.
15636 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15637 self.advance();
15638 }
15639 if cols.is_empty() {
15640 return Ok(None);
15641 }
15642 if is_unique {
15643 // Carry the captured idx_name on UNIQUE too so future
15644 // engine work can name the underlying BTree
15645 // accordingly; today the unique-constraint installer
15646 // synthesises the name itself, but Display round-trip
15647 // benefits from preserving it.
15648 Ok(Some(crate::ast::TableConstraint::Unique {
15649 name: idx_name,
15650 columns: cols,
15651 nulls_not_distinct: false,
15652 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15653 deferrable: false,
15654 initially_deferred: false,
15655 prefix_lengths,
15656 }))
15657 } else if is_fulltext {
15658 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15659 // routes through `TableConstraint::FulltextIndex`;
15660 // the engine builds a tsvector-GIN over each named
15661 // column so MATCH AGAINST gets a real inverted
15662 // index instead of a silently-dropped declaration.
15663 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15664 name: idx_name,
15665 columns: cols,
15666 }))
15667 } else if is_spatial {
15668 // SPG has no native SPATIAL AM. Accept-as-no-op
15669 // (declaration is parsed, but no index is built).
15670 Ok(None)
15671 } else {
15672 // v7.15.0 — plain KEY / INDEX builds a real BTree
15673 // secondary index.
15674 Ok(Some(crate::ast::TableConstraint::Index {
15675 name: idx_name,
15676 columns: cols,
15677 prefix_lengths,
15678 }))
15679 }
15680 }
15681
15682 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15683 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15684 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15685 /// (in any order, separated by whitespace).
15686 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15687 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15688 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15689 /// bare ident here, and only the parenthesised form is reloptions (so this
15690 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15691 fn consume_with_reloptions(&mut self) {
15692 let is_with = matches!(
15693 self.peek(),
15694 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15695 );
15696 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15697 return;
15698 }
15699 self.advance(); // WITH
15700 self.advance(); // (
15701 let mut depth = 1u32;
15702 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15703 match self.peek() {
15704 Token::LParen => depth += 1,
15705 Token::RParen => depth -= 1,
15706 _ => {}
15707 }
15708 self.advance();
15709 }
15710 }
15711
15712 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15713 /// dropped with everything else here. The rest of the MySQL table
15714 /// options genuinely have no meaning for SPG's storage; the engine
15715 /// name does, because MySQL REFUSES one it does not know and a dump
15716 /// with a typo in it should not quietly become a table.
15717 fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15718 let mut engine: Option<alloc::string::String> = None;
15719 let mut auto_increment: Option<i64> = None;
15720 loop {
15721 // Heuristic: a table option is an ident (or `DEFAULT`
15722 // reserved keyword) followed by `=` and an
15723 // ident / string / integer.
15724 let name_lc = match self.peek().clone() {
15725 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15726 Token::Default => alloc::string::String::from("default"),
15727 _ => break,
15728 };
15729 let known = matches!(
15730 name_lc.as_str(),
15731 "engine"
15732 | "default"
15733 | "charset"
15734 | "collate"
15735 | "auto_increment"
15736 | "row_format"
15737 | "comment"
15738 | "pack_keys"
15739 | "stats_persistent"
15740 | "stats_auto_recalc"
15741 | "stats_sample_pages"
15742 | "key_block_size"
15743 | "tablespace"
15744 | "min_rows"
15745 | "max_rows"
15746 | "checksum"
15747 | "delay_key_write"
15748 | "insert_method"
15749 | "data"
15750 | "index"
15751 | "encryption"
15752 | "compression"
15753 );
15754 if !known {
15755 break;
15756 }
15757 self.advance(); // option name
15758 // `DEFAULT` optional prefix is followed by `CHARSET` /
15759 // `COLLATE`; consume the next ident too.
15760 if name_lc == "default" {
15761 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15762 self.advance();
15763 }
15764 }
15765 if matches!(self.peek(), Token::Eq) {
15766 self.advance();
15767 }
15768 match self.peek().clone() {
15769 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15770 if name_lc == "engine" {
15771 // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15772 // engine it does not know and names it back
15773 // exactly: `Unknown storage engine 'NoSuchEng'`,
15774 // measured. The lexer folds a bare identifier, so
15775 // the message quoted a name the dump did not
15776 // contain, which is the one thing that message is
15777 // for. Guarded the same way the column spelling
15778 // is: the span runs to the next token, so what
15779 // comes back has to be the same word.
15780 let written = self
15781 .source_span(self.pos, self.pos)
15782 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15783 .filter(|raw| raw.eq_ignore_ascii_case(&v))
15784 .map(alloc::string::String::from);
15785 engine = Some(written.unwrap_or(v));
15786 }
15787 self.advance();
15788 }
15789 Token::Integer(v) => {
15790 // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15791 // the table hands out, and it was consumed and
15792 // dropped: measured on MySQL 9.7.2, the first row
15793 // inserted into a table declared that way gets 100,
15794 // where SPG gave it 1. `SHOW CREATE TABLE` already
15795 // reproduces the option from the counter, so the
15796 // dump round-tripped through a different number.
15797 if name_lc == "auto_increment" {
15798 auto_increment = Some(v);
15799 }
15800 self.advance();
15801 }
15802 _ => {}
15803 }
15804 }
15805 (engine, auto_increment)
15806 }
15807
15808 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15809 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15810 /// sure (otherwise a column literally named `primary` would
15811 /// be mistaken).
15812 fn peek_table_level_pk_start(&self) -> bool {
15813 let cur = self.peek();
15814 let nxt = self.tokens.get(self.pos + 1);
15815 let nxt2 = self.tokens.get(self.pos + 2);
15816 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15817 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15818 let is_lparen = matches!(nxt2, Some(Token::LParen));
15819 is_primary && is_key && is_lparen
15820 }
15821
15822 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15823 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15824 /// (mailrs round-5 G10).
15825 fn peek_table_level_unique_start(&self) -> bool {
15826 let cur = self.peek();
15827 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15828 if !is_unique {
15829 return false;
15830 }
15831 let n1 = self.tokens.get(self.pos + 1);
15832 // Plain `UNIQUE (…)`.
15833 if matches!(n1, Some(Token::LParen)) {
15834 return true;
15835 }
15836 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15837 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15838 if !is_nulls {
15839 return false;
15840 }
15841 let n2 = self.tokens.get(self.pos + 2);
15842 let n3 = self.tokens.get(self.pos + 3);
15843 let n4 = self.tokens.get(self.pos + 4);
15844 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15845 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15846 return true;
15847 }
15848 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15849 if matches!(n2, Some(Token::Not))
15850 && matches!(n3, Some(Token::Distinct))
15851 && matches!(n4, Some(Token::LParen))
15852 {
15853 return true;
15854 }
15855 false
15856 }
15857
15858 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15859 self.advance(); // PRIMARY
15860 self.advance(); // KEY
15861 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15862 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15863 // 621 consumed and dropped them (the storing half of F08).
15864 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15865 Ok(crate::ast::TableConstraint::PrimaryKey {
15866 name: None,
15867 columns,
15868 deferrable,
15869 initially_deferred,
15870 })
15871 }
15872
15873 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15874 self.advance(); // UNIQUE
15875 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15876 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15877 // is `NULLS DISTINCT` per the SQL standard.
15878 let mut nulls_not_distinct = false;
15879 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15880 let n1 = self.tokens.get(self.pos + 1);
15881 let n2 = self.tokens.get(self.pos + 2);
15882 let is_not = matches!(n1, Some(Token::Not));
15883 let is_distinct = matches!(n2, Some(Token::Distinct));
15884 if is_not && is_distinct {
15885 self.advance(); // NULLS
15886 self.advance(); // NOT
15887 self.advance(); // DISTINCT
15888 nulls_not_distinct = true;
15889 } else if matches!(n1, Some(Token::Distinct)) {
15890 self.advance(); // NULLS
15891 self.advance(); // DISTINCT
15892 }
15893 }
15894 let columns = self.parse_paren_ident_list("UNIQUE")?;
15895 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15896 Ok(crate::ast::TableConstraint::Unique {
15897 name: None,
15898 columns,
15899 nulls_not_distinct,
15900 deferrable,
15901 initially_deferred,
15902 prefix_lengths: Vec::new(),
15903 })
15904 }
15905
15906 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15907 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15908 /// expression.
15909 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15910 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15911 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15912 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15913 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15914 /// commit: `NOT` starts no other suffix here, but reading both
15915 /// tokens before advancing keeps the caller's error message intact
15916 /// if someone writes `NOT NULL` by mistake.
15917 fn parse_not_valid_suffix(&mut self) -> bool {
15918 if !matches!(self.peek(), Token::Not) {
15919 return false;
15920 }
15921 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15922 {
15923 return false;
15924 }
15925 self.advance();
15926 self.advance();
15927 true
15928 }
15929
15930 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15931 self.advance(); // EXCLUDE
15932 // Optional `USING <method>`.
15933 let mut method = None;
15934 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15935 self.advance();
15936 method = Some(match self.advance() {
15937 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15938 other => {
15939 return Err(self.err(alloc::format!(
15940 "expected index method after USING, got {other:?}"
15941 )));
15942 }
15943 });
15944 }
15945 if !matches!(self.peek(), Token::LParen) {
15946 return Err(self.err(alloc::format!(
15947 "expected '(' after EXCLUDE, got {:?}",
15948 self.peek()
15949 )));
15950 }
15951 self.advance();
15952 let mut elements: Vec<(String, String)> = Vec::new();
15953 loop {
15954 let col = match self.advance() {
15955 Token::Ident(s) | Token::QuotedIdent(s) => s,
15956 other => {
15957 return Err(self.err(alloc::format!(
15958 "expected column name in EXCLUDE, got {other:?}"
15959 )));
15960 }
15961 };
15962 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15963 return Err(self.err(alloc::format!(
15964 "expected WITH after EXCLUDE column, got {:?}",
15965 self.peek()
15966 )));
15967 }
15968 self.advance();
15969 let op = match self.advance() {
15970 Token::InetOverlap => String::from("&&"),
15971 Token::Intersects => String::from("?#"),
15972 Token::IsBelow => String::from("<^"),
15973 Token::IsAbove => String::from(">^"),
15974 Token::PatternLt => String::from("~<~"),
15975 Token::PatternLtEq => String::from("~<=~"),
15976 Token::PatternGt => String::from("~>~"),
15977 Token::PatternGtEq => String::from("~>=~"),
15978 Token::TsMatchOld => String::from("@@@"),
15979 Token::Eq => String::from("="),
15980 Token::JsonContains => String::from("@>"),
15981 Token::JsonContainedBy => String::from("<@"),
15982 Token::OverLeft => String::from("&<"),
15983 Token::OverRight => String::from("&>"),
15984 other => {
15985 return Err(self.err(alloc::format!(
15986 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15987 )));
15988 }
15989 };
15990 elements.push((col, op));
15991 if matches!(self.peek(), Token::Comma) {
15992 self.advance();
15993 continue;
15994 }
15995 break;
15996 }
15997 if !matches!(self.peek(), Token::RParen) {
15998 return Err(self.err(alloc::format!(
15999 "expected ')' to close EXCLUDE, got {:?}",
16000 self.peek()
16001 )));
16002 }
16003 self.advance();
16004 Ok(crate::ast::TableConstraint::Exclude {
16005 name: None,
16006 method,
16007 elements,
16008 })
16009 }
16010
16011 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
16012 self.advance(); // CHECK
16013 if !matches!(self.peek(), Token::LParen) {
16014 return Err(self.err(alloc::format!(
16015 "expected '(' after CHECK, got {:?}",
16016 self.peek()
16017 )));
16018 }
16019 self.advance();
16020 let expr = self.parse_expr(0)?;
16021 if !matches!(self.peek(), Token::RParen) {
16022 return Err(self.err(alloc::format!(
16023 "expected ')' to close CHECK predicate, got {:?}",
16024 self.peek()
16025 )));
16026 }
16027 self.advance();
16028 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
16029 // are no existing rows for PG to skip, so it rejects the suffix.
16030 Ok(crate::ast::TableConstraint::Check {
16031 name: None,
16032 expr,
16033 not_valid: false,
16034 })
16035 }
16036
16037 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
16038 fn peek_table_level_check_start(&self) -> bool {
16039 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
16040 }
16041
16042 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
16043 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
16044 /// on the dedicated FK path (`parse_table_level_fk` consumes its
16045 /// own CONSTRAINT prefix).
16046 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16047 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16048 return None;
16049 }
16050 // tokens[pos+1] is the constraint name (any ident-like);
16051 // tokens[pos+2] is the kind keyword.
16052 match self.tokens.get(self.pos + 2) {
16053 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16054 Some(NamedTableConstraintKind::Check)
16055 }
16056 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16057 Some(NamedTableConstraintKind::Unique)
16058 }
16059 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16060 Some(NamedTableConstraintKind::PrimaryKey)
16061 }
16062 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16063 Some(NamedTableConstraintKind::Exclude)
16064 }
16065 _ => None,
16066 }
16067 }
16068
16069 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16070 if !matches!(self.peek(), Token::LParen) {
16071 return Err(self.err(alloc::format!(
16072 "expected '(' after {ctx}, got {:?}",
16073 self.peek()
16074 )));
16075 }
16076 self.advance();
16077 let mut out = Vec::new();
16078 loop {
16079 out.push(self.expect_ident_like()?);
16080 match self.peek() {
16081 Token::Comma => {
16082 self.advance();
16083 }
16084 Token::RParen => {
16085 self.advance();
16086 break;
16087 }
16088 other => {
16089 return Err(self.err(alloc::format!(
16090 "expected ',' or ')' in {ctx} list, got {other:?}"
16091 )));
16092 }
16093 }
16094 }
16095 if out.is_empty() {
16096 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16097 }
16098 Ok(out)
16099 }
16100
16101 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16102 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16103 /// table-level FK; a column def never starts with either keyword
16104 /// (column names are not in this reserved set).
16105 fn peek_constraint_or_fk_start(&self) -> bool {
16106 let is_constraint_kw = matches!(
16107 self.peek(),
16108 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16109 );
16110 let is_foreign_kw = matches!(
16111 self.peek(),
16112 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16113 );
16114 is_constraint_kw || is_foreign_kw
16115 }
16116
16117 /// v7.6.0 — parse a table-level FK clause:
16118 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16119 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16120 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16121 let mut name: Option<String> = None;
16122 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16123 self.advance();
16124 name = Some(self.expect_ident_like()?);
16125 }
16126 // `FOREIGN`
16127 match self.advance() {
16128 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16129 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16130 }
16131 // `KEY`
16132 match self.advance() {
16133 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16134 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16135 }
16136 // `(col, col, ...)`
16137 if !matches!(self.peek(), Token::LParen) {
16138 return Err(self.err(format!(
16139 "expected '(' after FOREIGN KEY, got {:?}",
16140 self.peek()
16141 )));
16142 }
16143 self.advance();
16144 let mut columns = Vec::new();
16145 loop {
16146 columns.push(self.expect_ident_like()?);
16147 match self.peek() {
16148 Token::Comma => {
16149 self.advance();
16150 }
16151 Token::RParen => {
16152 self.advance();
16153 break;
16154 }
16155 other => {
16156 return Err(self.err(format!(
16157 "expected ',' or ')' in FK column list, got {other:?}"
16158 )));
16159 }
16160 }
16161 }
16162 if columns.is_empty() {
16163 return Err(self.err("FOREIGN KEY requires at least one column".into()));
16164 }
16165 let (
16166 parent_table,
16167 parent_columns,
16168 on_delete,
16169 on_update,
16170 match_type,
16171 deferrable,
16172 initially_deferred,
16173 ) = self.parse_references_tail(columns.len())?;
16174 Ok(ForeignKeyConstraint {
16175 name,
16176 columns,
16177 parent_table,
16178 parent_columns,
16179 on_delete,
16180 on_update,
16181 match_type,
16182 deferrable,
16183 initially_deferred,
16184 })
16185 }
16186
16187 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16188 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16189 /// the local column count, used to default the parent column
16190 /// list when omitted (SQL spec: parent's PK is implied).
16191 fn parse_references_tail(
16192 &mut self,
16193 expected_arity: usize,
16194 ) -> Result<
16195 (
16196 String,
16197 Vec<String>,
16198 FkAction,
16199 FkAction,
16200 crate::ast::MatchType,
16201 // v7.39 (round 288) — deferrable, initially_deferred.
16202 bool,
16203 bool,
16204 ),
16205 ParseError,
16206 > {
16207 match self.advance() {
16208 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16209 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16210 }
16211 let parent_table = self.expect_ident_like()?;
16212 let mut parent_columns: Vec<String> = Vec::new();
16213 if matches!(self.peek(), Token::LParen) {
16214 self.advance();
16215 loop {
16216 parent_columns.push(self.expect_ident_like()?);
16217 match self.peek() {
16218 Token::Comma => {
16219 self.advance();
16220 }
16221 Token::RParen => {
16222 self.advance();
16223 break;
16224 }
16225 other => {
16226 return Err(self.err(format!(
16227 "expected ',' or ')' in REFERENCES column list, got {other:?}"
16228 )));
16229 }
16230 }
16231 }
16232 }
16233 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16234 return Err(self.err(format!(
16235 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16236 expected_arity,
16237 parent_columns.len()
16238 )));
16239 }
16240 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16241 // it between the referenced column list and the ON / DEFERRABLE
16242 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16243 // is skipped when any referencing column is NULL), so SIMPLE —
16244 // the default, and the only spelling pg_dump emits — is accepted
16245 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16246 // mixed-NULL rule, which is not wired yet; reject them honestly
16247 // rather than silently applying SIMPLE (PG itself errors on
16248 // MATCH PARTIAL as "not yet implemented").
16249 let mut match_type = crate::ast::MatchType::Simple;
16250 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16251 self.advance();
16252 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16253 // SIMPLE / PARTIAL arrive as bare identifiers.
16254 let kind = match self.advance() {
16255 Token::Full => "FULL".to_string(),
16256 Token::Ident(s) => s.to_uppercase(),
16257 other => {
16258 return Err(self.err(format!(
16259 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16260 )));
16261 }
16262 };
16263 match kind.as_str() {
16264 "SIMPLE" => {} // Default — match_type stays Simple.
16265 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16266 // when ALL referencing columns are NULL; a mixed-NULL key errors.
16267 "FULL" => match_type = crate::ast::MatchType::Full,
16268 "PARTIAL" => {
16269 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16270 }
16271 _ => {
16272 return Err(self.err(format!(
16273 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16274 )));
16275 }
16276 }
16277 }
16278 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16279 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16280 // <action>` / `ON UPDATE <action>` in either order. PG /
16281 // pg_dump emits the timing clause AFTER the ON clauses
16282 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16283 // but the SQL spec allows either order. We loop over
16284 // every possible trailer and dispatch on the next token,
16285 // stopping when nothing matches. Phase 3.1 changes the
16286 // bare DEFERRABLE form from hard-error to accept-as-
16287 // immediate; SPG is single-writer with no deferred-
16288 // constraint window so the runtime semantics are always
16289 // immediate even when INITIALLY DEFERRED is requested.
16290 // PG's default referential action (no ON DELETE / ON UPDATE
16291 // clause) is NO ACTION, not RESTRICT — the two enforce
16292 // identically in SPG (single-writer, no deferred window; see the
16293 // shared match arm in constraints.rs) but information_schema.
16294 // referential_constraints must report NO ACTION to match PG.
16295 let mut on_delete = FkAction::NoAction;
16296 let mut on_update = FkAction::NoAction;
16297 let mut seen_on_delete = false;
16298 let mut seen_on_update = false;
16299 let mut deferrable = false;
16300 let mut initially_deferred = false;
16301 loop {
16302 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16303 let before = self.pos;
16304 let (d, idef) = self.consume_deferrable_clauses_timed()?;
16305 if self.pos != before {
16306 deferrable = d;
16307 initially_deferred = idef;
16308 continue;
16309 }
16310 // ON DELETE / ON UPDATE.
16311 if !matches!(self.peek(), Token::On) {
16312 break;
16313 }
16314 self.advance();
16315 let which = self.advance();
16316 let action = self.parse_fk_action()?;
16317 match which {
16318 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16319 if seen_on_delete {
16320 return Err(self.err("ON DELETE specified twice".into()));
16321 }
16322 seen_on_delete = true;
16323 on_delete = action;
16324 }
16325 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16326 if seen_on_update {
16327 return Err(self.err("ON UPDATE specified twice".into()));
16328 }
16329 seen_on_update = true;
16330 on_update = action;
16331 }
16332 other => {
16333 return Err(
16334 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16335 );
16336 }
16337 }
16338 }
16339 Ok((
16340 parent_table,
16341 parent_columns,
16342 on_delete,
16343 on_update,
16344 match_type,
16345 deferrable,
16346 initially_deferred,
16347 ))
16348 }
16349
16350 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16351 /// NO ACTION`.
16352 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16353 match self.advance() {
16354 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16355 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16356 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16357 Token::Null => Ok(FkAction::SetNull),
16358 Token::Default => Ok(FkAction::SetDefault),
16359 other => Err(self.err(format!(
16360 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16361 ))),
16362 },
16363 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16364 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16365 other => Err(self.err(format!(
16366 "expected ACTION after NO in FK action, got {other:?}"
16367 ))),
16368 },
16369 other => Err(self.err(format!(
16370 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16371 ))),
16372 }
16373 }
16374
16375 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16376 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16377 fn consume_if_not_exists(&mut self) -> bool {
16378 // `IF` arrives as a bare Ident (we don't reserve it because it
16379 // also appears mid-expression in PG, though we don't support
16380 // those forms yet).
16381 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16382 if !looks_like_if {
16383 return false;
16384 }
16385 // Peek one ahead before committing: only consume IF when it's
16386 // actually `IF NOT EXISTS`.
16387 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16388 return false;
16389 }
16390 if !matches!(
16391 self.tokens.get(self.pos + 2),
16392 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16393 ) {
16394 return false;
16395 }
16396 self.advance(); // IF
16397 self.advance(); // NOT
16398 self.advance(); // EXISTS
16399 true
16400 }
16401
16402 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16403 /// Consumes IF EXISTS as a pair; returns false otherwise
16404 /// without consuming any tokens.
16405 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16406 /// ENABLE/DISABLE/FORCE/NO FORCE.
16407 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16408 for kw in ["row", "level", "security"] {
16409 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16410 {
16411 return Err(self.err(alloc::format!(
16412 "expected {} in ROW LEVEL SECURITY, got {:?}",
16413 kw.to_ascii_uppercase(),
16414 self.peek()
16415 )));
16416 }
16417 self.advance();
16418 }
16419 Ok(())
16420 }
16421
16422 fn consume_if_exists(&mut self) -> bool {
16423 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16424 if !looks_like_if {
16425 return false;
16426 }
16427 if !matches!(
16428 self.tokens.get(self.pos + 1),
16429 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16430 ) {
16431 return false;
16432 }
16433 self.advance(); // IF
16434 self.advance(); // EXISTS
16435 true
16436 }
16437
16438 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16439 /// qualifiers after an index column ref. ASC / DESC are
16440 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16441 /// We accept and discard them since single-column BTree
16442 /// stores rows in natural key order today.
16443 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16444 /// ORDER BY key. Returns None when absent.
16445 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16446 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16447 return Ok(None);
16448 }
16449 self.advance();
16450 match self.advance() {
16451 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16452 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16453 other => Err(self.err(alloc::format!(
16454 "expected FIRST or LAST after NULLS, got {other:?}"
16455 ))),
16456 }
16457 }
16458
16459 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16460 /// rather than discarded.
16461 ///
16462 /// SPG's index does not scan in a direction — column ordering is
16463 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16464 /// reproduction of the DDL, and dropping the clause meant
16465 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16466 /// dump lost it, and a schema diff saw drift on every run.
16467 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16468 let mut order = crate::ast::IndexColumnOrder::default();
16469 loop {
16470 match self.peek() {
16471 Token::Asc => {
16472 self.advance();
16473 }
16474 Token::Desc => {
16475 order.descending = true;
16476 self.advance();
16477 }
16478 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16479 let look = self.tokens.get(self.pos + 1);
16480 if matches!(
16481 look,
16482 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16483 || k.eq_ignore_ascii_case("last")
16484 ) {
16485 self.advance();
16486 order.nulls_first = Some(matches!(
16487 self.advance(),
16488 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16489 ));
16490 } else {
16491 break;
16492 }
16493 }
16494 _ => break,
16495 }
16496 }
16497 order
16498 }
16499
16500 fn parse_create_index_stmt_after_create(
16501 &mut self,
16502 is_unique: bool,
16503 ) -> Result<Statement, ParseError> {
16504 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16505 debug_assert!(matches!(self.peek(), Token::Index));
16506 self.advance();
16507 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16508 // SPG's CREATE INDEX is synchronous end-to-end today (real
16509 // CONCURRENTLY variant with restartable scans queues with
16510 // v7.39 indexes epic), so the modifier has no runtime effect
16511 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16512 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16513 // VIEW CONCURRENTLY.
16514 let mut concurrently = false;
16515 if matches!(
16516 self.peek(),
16517 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16518 ) {
16519 self.advance();
16520 concurrently = true;
16521 }
16522 let if_not_exists = self.consume_if_not_exists();
16523 // v7.39 (read01 round 93) — the index name is optional (PG since
16524 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16525 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16526 // was given; leave it empty and the engine derives a PG-style
16527 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16528 let name = if matches!(self.peek(), Token::On) {
16529 String::new()
16530 } else {
16531 self.expect_ident_like()?
16532 };
16533 if !matches!(self.peek(), Token::On) {
16534 return Err(self.err(format!(
16535 "expected ON after CREATE INDEX <name>, got {:?}",
16536 self.peek()
16537 )));
16538 }
16539 self.advance();
16540 let table = self.expect_ident_like()?;
16541 // Optional `USING <method>` — only recognised method in v2.0 is
16542 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16543 // ident `using` (we don't promote it to a reserved keyword
16544 // because it isn't reserved anywhere else in our SQL surface).
16545 let mut method_name: Option<String> = None;
16546 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16547 self.advance();
16548 let m = self.expect_ident_like()?;
16549 method_name = Some(m.to_ascii_lowercase());
16550 match m.to_ascii_lowercase().as_str() {
16551 "hnsw" => IndexMethod::Hnsw,
16552 "btree" => IndexMethod::BTree,
16553 "brin" => IndexMethod::Brin,
16554 // v7.12.3 — real GIN inverted index over `tsvector`.
16555 // v7.9.26b's `USING gin` → BTree silent fallback is
16556 // gone; the engine validates that the indexed column
16557 // is `tsvector` at CREATE INDEX time.
16558 "gin" => IndexMethod::Gin,
16559 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16560 // `USING spgist` / `USING hash` for their built-in
16561 // AMs that SPG doesn't have a matching
16562 // implementation for; degrade to BTree on the
16563 // leading column so the schema loads + the index
16564 // catalogue stays consistent. Operator pays the
16565 // planner cost only for the queries that would have
16566 // used the specialised AM.
16567 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16568 // v7.11.3 — pgvector ships both `ivfflat` and
16569 // `hnsw`. Customers shouldn't have to choose
16570 // their on-disk index method based on what SPG
16571 // implements; accept `ivfflat` as a synonym for
16572 // `hnsw` so PG schemas using either method drop
16573 // in. The vector distance op (`<->` / `<#>` /
16574 // `<=>`) at query time still picks the metric.
16575 "ivfflat" => IndexMethod::Hnsw,
16576 other => {
16577 return Err(self.err(alloc::format!(
16578 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16579 )));
16580 }
16581 }
16582 } else {
16583 IndexMethod::BTree
16584 };
16585 if !matches!(self.peek(), Token::LParen) {
16586 return Err(self.err(format!(
16587 "expected '(' before indexed column, got {:?}",
16588 self.peek()
16589 )));
16590 }
16591 self.advance();
16592 // v6.8.2 — accept either a bare column ident (legacy) or
16593 // an expression `fn(col, …)` for expression indexes.
16594 // Distinguish by peeking the token *after* the current
16595 // ident: `ident )` is the legacy column-only path;
16596 // anything else triggers the Pratt expression parser.
16597 // (`advance()` uses `mem::replace` to nil out the current
16598 // slot, so we can't save+rewind cleanly — peek-ahead via
16599 // direct index avoids the mutation.)
16600 let mut opclass: Option<String> = None;
16601 let mut key_collation: Option<String> = None;
16602 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16603 // Single column with `)` immediately after — fast path.
16604 // v7.9.29 — also: bare column followed by `,` (the
16605 // multi-column form `(a, b, c)`). Without this branch
16606 // the leading ident gets pulled into `parse_expr`
16607 // which then sets `expression = Some(Column(a))` and
16608 // breaks Display round-trip on the multi-column shape.
16609 Token::Ident(s) | Token::QuotedIdent(s)
16610 if matches!(
16611 self.tokens.get(self.pos + 1),
16612 Some(Token::RParen | Token::Comma)
16613 ) =>
16614 {
16615 self.advance();
16616 (s, None)
16617 }
16618 // v7.9.22 — single column followed by a pgvector
16619 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16620 // v7.15.0 — capture the opclass instead of discarding
16621 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16622 // → real trigram-shingle GIN over a TEXT column).
16623 // Vector/HNSW opclasses still take their distance
16624 // metric from the query operator (`<->` / `<#>` /
16625 // `<=>`), so for those callers the opclass stays
16626 // informational.
16627 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16628 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16629 // the schema and dispatch on the bare opclass, the same
16630 // treatment table/type names get.
16631 Token::Ident(s) | Token::QuotedIdent(s)
16632 if matches!(
16633 self.tokens.get(self.pos + 1),
16634 Some(Token::Ident(_) | Token::QuotedIdent(_))
16635 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16636 && matches!(
16637 self.tokens.get(self.pos + 3),
16638 Some(Token::Ident(op) | Token::QuotedIdent(op))
16639 if is_vector_opclass_name(op)
16640 ) =>
16641 {
16642 self.advance(); // column name
16643 self.advance(); // schema qualifier
16644 self.advance(); // dot
16645 let op_tok = self.advance();
16646 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16647 opclass = Some(op.to_ascii_lowercase());
16648 }
16649 (s, None)
16650 }
16651 // r1038 — an operator class is recognised by its POSITION, not
16652 // by a list of names. It used to be `is_vector_opclass_name`,
16653 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16654 // sentori's migration wrote — was a syntax error while
16655 // `USING gin (doc)` parsed. Anything sitting between a column
16656 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16657 // two bare identifiers in a row are not valid there otherwise.
16658 Token::Ident(s) | Token::QuotedIdent(s)
16659 if matches!(
16660 self.tokens.get(self.pos + 1),
16661 Some(Token::Ident(op) | Token::QuotedIdent(op))
16662 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16663 self.tokens.get(self.pos + 2)
16664 )
16665 ) =>
16666 {
16667 self.advance(); // column name
16668 // Capture the opclass token, lower-cased for
16669 // case-insensitive engine dispatch.
16670 let op_tok = self.advance();
16671 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16672 opclass = Some(op.to_ascii_lowercase());
16673 }
16674 (s, None)
16675 }
16676 Token::Ident(_) | Token::QuotedIdent(_) => {
16677 // v7.39 (round 538) — an explicit COLLATE on the key,
16678 // read by LOOKAHEAD because `parse_expr` absorbs the
16679 // clause as a no-op (SPG orders text by bytes, which is
16680 // the C collation, so it changes nothing to honour). PG
16681 // still PRINTS it: an explicitly written `"C"` and the
16682 // collation a column inherits are different collation
16683 // OBJECTS even where they sort identically, which is why
16684 // `(a COLLATE "C")` shows on a C-collation database too.
16685 if matches!(
16686 self.tokens.get(self.pos + 1),
16687 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16688 ) {
16689 key_collation = match self.tokens.get(self.pos + 2) {
16690 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16691 Some(n.clone())
16692 }
16693 _ => None,
16694 };
16695 }
16696 // v7.39.2 — the clause is read by the LOOKAHEAD above and
16697 // belongs to the KEY, not to the expression. Since
16698 // `COLLATE` became a node, letting `parse_expr` build one
16699 // here put the collation in twice and the key deparsed as
16700 // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16701 // is the same idea and already exists, so this borrows it:
16702 // absorb into the side channel, and the key's own
16703 // lookahead is what carries it.
16704 // v7.39.2 — and the key can only CARRY the byte-order
16705 // spellings. Absorbing into the side channel accepts any
16706 // name, so suppressing the node here without this check
16707 // silently accepted `(name COLLATE "en_US")`, which SPG's
16708 // index cannot honour — a refusal that was doing real
16709 // work, removed by the suppression and put back here.
16710 if let Some(name) = &key_collation {
16711 let lc = name.to_ascii_lowercase();
16712 let byte_order = matches!(
16713 lc.as_str(),
16714 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16715 );
16716 let mysql_ok = self.mysql_dialect
16717 && (lc.ends_with("_ci")
16718 || lc.ends_with("_bin")
16719 || lc == "binary"
16720 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16721 if !byte_order && !mysql_ok {
16722 return Err(self.err(alloc::format!(
16723 "COLLATE {name:?} is not supported in this position: an index \
16724 key carries the byte-order spellings only. Declare it on the \
16725 column (`x text COLLATE {name:?}`) instead"
16726 )));
16727 }
16728 }
16729 let saved_key_ctx = self.in_order_by_key;
16730 self.in_order_by_key = true;
16731 let key_expr = self.parse_expr(0);
16732 self.in_order_by_key = saved_key_ctx;
16733 let key_expr = key_expr?;
16734 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16735 self.err("expression index key must reference at least one column".into())
16736 })?;
16737 (primary, Some(key_expr))
16738 }
16739 // v7.37.43-T4 — parenthesised expression index key
16740 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16741 // PG's CREATE INDEX requires the expression to be in
16742 // its own parens to disambiguate function calls from
16743 // column lists, so this `LParen` is the inner open-paren
16744 // of an expression key. parse_expr handles the recursive
16745 // descent and consumes the matching `RParen`.
16746 Token::LParen => {
16747 let key_expr = self.parse_expr(0)?;
16748 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16749 self.err("expression index key must reference at least one column".into())
16750 })?;
16751 (primary, Some(key_expr))
16752 }
16753 other => {
16754 return Err(self.err(format!(
16755 "expected column ident or expression, got {other:?}"
16756 )));
16757 }
16758 };
16759 // v7.9.14 — accept extra comma-separated columns inside
16760 // the index key parens (`CREATE INDEX … (a, b, c)`).
16761 // mailrs F2.
16762 //
16763 // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16764 // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16765 // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16766 // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16767 // schema diff saw drift on every run. Reported by sentori
16768 // against 7.39.10, and the same defect round 537 fixed for the
16769 // LEADING column, in the loop right beside it.
16770 let mut extra_columns: Vec<String> = Vec::new();
16771 let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16772 // The leading column may also have ASC/DESC after it — and that
16773 // one is the column SPG indexes, so its clause is kept.
16774 let key_order = self.consume_optional_index_column_qualifiers();
16775 while matches!(self.peek(), Token::Comma) {
16776 self.advance();
16777 let extra = self.expect_ident_like()?;
16778 extra_orders.push(self.consume_optional_index_column_qualifiers());
16779 extra_columns.push(extra);
16780 }
16781 if !matches!(self.peek(), Token::RParen) {
16782 return Err(self.err(format!(
16783 "expected ')' after indexed column / expression, got {:?}",
16784 self.peek()
16785 )));
16786 }
16787 self.advance();
16788 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16789 // index-only-scan annotation. Bare ident (not a reserved
16790 // keyword) so we test by case-insensitive string match.
16791 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16792 {
16793 self.advance();
16794 if !matches!(self.peek(), Token::LParen) {
16795 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16796 }
16797 self.advance();
16798 let mut cols = Vec::new();
16799 loop {
16800 cols.push(self.expect_ident_like()?);
16801 match self.peek() {
16802 Token::Comma => {
16803 self.advance();
16804 }
16805 Token::RParen => {
16806 self.advance();
16807 break;
16808 }
16809 other => {
16810 return Err(self.err(format!(
16811 "expected ',' or ')' in INCLUDE list, got {other:?}"
16812 )));
16813 }
16814 }
16815 }
16816 cols
16817 } else {
16818 Vec::new()
16819 };
16820 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16821 // storage parameters. pgvector emits `WITH (lists = N)` for
16822 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16823 // SPG's HNSW picks its own parameters today (tunable via
16824 // env vars), so the WITH clause is informational and dropped.
16825 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16826 self.advance();
16827 if !matches!(self.peek(), Token::LParen) {
16828 return Err(self.err(format!(
16829 "expected '(' after WITH in CREATE INDEX, got {:?}",
16830 self.peek()
16831 )));
16832 }
16833 self.advance();
16834 loop {
16835 if matches!(self.peek(), Token::RParen) {
16836 self.advance();
16837 break;
16838 }
16839 // Drain `key = value` or bare `key` tokens.
16840 let _ = self.advance(); // key
16841 if matches!(self.peek(), Token::Eq) {
16842 self.advance();
16843 let _ = self.advance(); // value (int / string / ident)
16844 }
16845 match self.peek() {
16846 Token::Comma => {
16847 self.advance();
16848 }
16849 Token::RParen => {
16850 self.advance();
16851 break;
16852 }
16853 other => {
16854 return Err(self.err(format!(
16855 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16856 )));
16857 }
16858 }
16859 }
16860 }
16861 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16862 // which sits between the key list and the WHERE clause.
16863 let mut nulls_not_distinct = false;
16864 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16865 let n1 = self.tokens.get(self.pos + 1);
16866 let n2 = self.tokens.get(self.pos + 2);
16867 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16868 self.advance(); // NULLS
16869 self.advance(); // NOT
16870 self.advance(); // DISTINCT
16871 nulls_not_distinct = true;
16872 } else if matches!(n1, Some(Token::Distinct)) {
16873 self.advance(); // NULLS
16874 self.advance(); // DISTINCT
16875 }
16876 }
16877 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16878 let partial_predicate = if matches!(self.peek(), Token::Where) {
16879 self.advance();
16880 Some(self.parse_expr(0)?)
16881 } else {
16882 None
16883 };
16884 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16885 // sense: uniqueness over an ANN structure has no clean
16886 // semantics. Reject early. (BRIN UNIQUE is similarly
16887 // meaningless — block both.)
16888 if is_unique && !matches!(method, IndexMethod::BTree) {
16889 return Err(self.err(alloc::format!(
16890 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16891 method
16892 )));
16893 }
16894 Ok(Statement::CreateIndex(CreateIndexStatement {
16895 concurrently,
16896 name,
16897 key_order,
16898 key_collation,
16899 table,
16900 column,
16901 nulls_not_distinct,
16902 method,
16903 if_not_exists,
16904 included_columns,
16905 partial_predicate,
16906 extra_columns: extra_columns.clone(),
16907 extra_orders: extra_orders.clone(),
16908 expression,
16909 is_unique,
16910 opclass,
16911 method_name,
16912 }))
16913 }
16914
16915 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16916 /// column-level `REFERENCES ...` clause. The trailing FK is
16917 /// normalised into table-level shape (single-element columns +
16918 /// parent_columns) so the engine sees one uniform constraint list.
16919 fn parse_column_def_with_fk(
16920 &mut self,
16921 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16922 let col = self.parse_column_def()?;
16923 // v7.39 (round 308, V29) — an explicitly named inline FK:
16924 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16925 // loop leaves this spelling intact precisely so the name can be
16926 // kept here; PG reports it in violation messages and matches it
16927 // in `SET CONSTRAINTS`.
16928 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16929 {
16930 self.advance();
16931 Some(self.expect_ident_like()?)
16932 } else {
16933 None
16934 };
16935 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16936 let inline_references = matches!(
16937 self.peek(),
16938 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16939 );
16940 if !inline_references {
16941 return Ok((col, None));
16942 }
16943 let (
16944 parent_table,
16945 parent_columns,
16946 on_delete,
16947 on_update,
16948 match_type,
16949 deferrable,
16950 initially_deferred,
16951 ) = self.parse_references_tail(1)?;
16952 let fk = ForeignKeyConstraint {
16953 name: declared_name,
16954 columns: vec![col.name.clone()],
16955 parent_table,
16956 parent_columns,
16957 on_delete,
16958 on_update,
16959 match_type,
16960 deferrable,
16961 initially_deferred,
16962 };
16963 Ok((col, Some(fk)))
16964 }
16965
16966 /// v7.13.0 — parse a column type (consuming the type ident and
16967 /// any trailing parameters / `[]`), without surrounding column
16968 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16969 /// Returns the resolved `ColumnTypeName` plus implied
16970 /// `(auto_increment, not_null)` flags from PG SERIAL family
16971 /// shorthands — callers that don't expect those (ALTER COLUMN
16972 /// TYPE) can discard them.
16973 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16974 let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16975 Ok(ty)
16976 }
16977
16978 #[allow(clippy::type_complexity)]
16979 fn parse_type_with_implied_flags(
16980 &mut self,
16981 ) -> Result<
16982 (
16983 ColumnTypeName,
16984 bool,
16985 bool,
16986 Option<String>,
16987 Collation,
16988 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16989 bool,
16990 // v7.39 (round 676) — the collation NAME as written, which the
16991 // `Collation` enum above cannot carry.
16992 Option<String>,
16993 bool,
16994 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16995 // list captured at type-parse time. None for all
16996 // non-ENUM types.
16997 Option<Vec<String>>,
16998 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16999 // list. Distinct from ENUM (subset semantics).
17000 Option<Vec<String>>,
17001 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
17002 // width, lost when the type collapses to SmallInt / Int.
17003 Option<MysqlIntWidth>,
17004 // v7.39 (round 424) — declared fractional-seconds precision of a
17005 // MySQL temporal column (bare spelling = 0). None under PG.
17006 Option<u8>,
17007 // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
17008 // two are different types on MySQL and SPG stores both as
17009 // `Timestamp`, so the spelling has to travel separately or
17010 // a dump silently rewrites the column.
17011 bool,
17012 // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
17013 // display hint: it rounds on write.
17014 Option<(u8, u8)>,
17015 ),
17016 ParseError,
17017 > {
17018 let mut ty_ident = match self.advance() {
17019 Token::Ident(s) => s,
17020 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
17021 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
17022 // '<span>'` literal grammar. As a column type it lands
17023 // here directly; downstream resolution still uses the
17024 // canonical lowercase string.
17025 Token::Interval => "interval".to_string(),
17026 other => {
17027 return Err(ParseError {
17028 message: format!("expected column type, got {other:?}"),
17029 token_pos: self.consumed_pos(),
17030 });
17031 }
17032 };
17033 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
17034 // pg_dump qualifies extension types (`public.vector(1024)`).
17035 // SPG is single-namespace; drop the schema and resolve the
17036 // bare type — same treatment table names already get.
17037 while matches!(self.peek(), Token::Dot) {
17038 self.advance();
17039 ty_ident = self.expect_ident_like()?;
17040 }
17041 let mut implied_auto_increment = false;
17042 let mut implied_not_null = false;
17043 let mut user_type_ref: Option<String> = None;
17044 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17045 // value list, captured here and bubbled up through the
17046 // ColumnDef so the engine can attach it to the column
17047 // schema (and validate INSERT cells against it).
17048 let mut inline_enum_variants: Option<Vec<String>> = None;
17049 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17050 let mut inline_set_variants: Option<Vec<String>> = None;
17051 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17052 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17053 // collapses to SmallInt / Int. Only under the MySQL dialect.
17054 let mut mysql_int_width: Option<MysqlIntWidth> = None;
17055 // v7.39 (round 424) — the declared fractional-seconds precision of a
17056 // MySQL temporal column. Set by the temporal arms below; stays None
17057 // for PG (whose temporal columns keep full microseconds).
17058 let mut mysql_fsp: Option<u8> = None;
17059 let mut mysql_declared_timestamp = false;
17060 let mut mysql_float_md: Option<(u8, u8)> = None;
17061 let mut ty = match ty_ident.as_str() {
17062 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17063 "smallserial" | "serial2" => {
17064 implied_auto_increment = true;
17065 implied_not_null = true;
17066 ColumnTypeName::SmallInt
17067 }
17068 "serial" | "serial4" => {
17069 implied_auto_increment = true;
17070 implied_not_null = true;
17071 ColumnTypeName::Int
17072 }
17073 "bigserial" | "serial8" => {
17074 implied_auto_increment = true;
17075 implied_not_null = true;
17076 ColumnTypeName::BigInt
17077 }
17078 // MySQL flavours we accept by aliasing to the closest SPG
17079 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17080 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17081 // 24-bit) → INT. UNSIGNED modifiers are consumed below
17082 // without semantic effect.
17083 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17084 // PG's internal type names; pg_dump and hand-written PG schemas
17085 // use them interchangeably with smallint / int / bigint (the cast
17086 // path already accepted them, only the column grammar didn't).
17087 "smallint" | "int2" => {
17088 // v7.14.0 — MySQL display-width on integers
17089 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17090 // parenthesised number is purely cosmetic — it
17091 // doesn't change storage. Accept + discard.
17092 self.consume_optional_paren_size();
17093 ColumnTypeName::SmallInt
17094 }
17095 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17096 // canonical encoding for BOOLEAN. Every MySQL driver
17097 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17098 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17099 // 4.3 SPG classified TINYINT(1) as SmallInt, which
17100 // gave the customer i16-shaped values where the app
17101 // expected bool — a Tier-A silent type drift on
17102 // mysqldump restores. Now: `TINYINT(1)` → Bool;
17103 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17104 // stay SmallInt (the legacy width-agnostic path).
17105 "tinyint" => {
17106 let width = self.peek_optional_paren_size_value();
17107 self.consume_optional_paren_size();
17108 if width == Some(1) {
17109 ColumnTypeName::Bool
17110 } else {
17111 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17112 // lost width so the write path can enforce -128..127.
17113 if self.mysql_dialect {
17114 mysql_int_width = Some(MysqlIntWidth::Tiny);
17115 }
17116 ColumnTypeName::SmallInt
17117 }
17118 }
17119 "mediumint" => {
17120 self.consume_optional_paren_size();
17121 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17122 if self.mysql_dialect {
17123 mysql_int_width = Some(MysqlIntWidth::Medium);
17124 }
17125 ColumnTypeName::Int
17126 }
17127 "int" | "integer" | "int4" => {
17128 self.consume_optional_paren_size();
17129 ColumnTypeName::Int
17130 }
17131 "bigint" | "int8" => {
17132 self.consume_optional_paren_size();
17133 ColumnTypeName::BigInt
17134 }
17135 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17136 // (mailrs round-5 G6). Consume the optional `PRECISION`
17137 // tail when the type keyword was `double` / `DOUBLE`.
17138 //
17139 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17140 // FLOAT". `FLOAT(p)` picks the width the way PG does:
17141 // p in 1..=24 is real, 25..=53 is double precision, and
17142 // anything else is an error.
17143 "float" | "double" | "real" => {
17144 if ty_ident.eq_ignore_ascii_case("double")
17145 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17146 {
17147 self.advance();
17148 }
17149 if ty_ident.eq_ignore_ascii_case("real") {
17150 // v7.39 (round 274) — the two dialects genuinely
17151 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17152 // synonym for DOUBLE (8-byte). Round 269 made REAL
17153 // 32-bit globally and thereby narrowed the stored
17154 // precision of every MySQL REAL column.
17155 if self.mysql_dialect {
17156 ColumnTypeName::Float
17157 } else {
17158 ColumnTypeName::Real
17159 }
17160 } else if self.mysql_dialect
17161 && matches!(self.peek(), Token::LParen)
17162 && self.peek_paren_has_comma()
17163 {
17164 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17165 // display form (`FLOAT(10,2)`), which PG has no
17166 // equivalent of. It was `syntax error at or near ","`,
17167 // so the whole CREATE failed.
17168 //
17169 // v7.39.2 — the guard said `float` while the comment
17170 // said both, so `DOUBLE(10,2)` — which every legacy
17171 // MySQL schema uses for money — still failed the
17172 // whole CREATE with `syntax error at or near "("`.
17173 // Measured on 9.7.2: both forms are accepted, and the
17174 // digits are NOT a display hint, they round on write
17175 // (3.14159265358979 into either stores 3.14). The
17176 // rounding is recorded as a residual; accepting the
17177 // syntax and keeping the width is the half this
17178 // change makes.
17179 // v7.39.3 — keep the pair. The digits are not a
17180 // display hint: MySQL 9.7.2 ROUNDS on write and
17181 // refuses a value wider than `m` (errno 1264), so a
17182 // column declared for money held more precision here
17183 // than its schema said.
17184 let (m, d) = self.parse_optional_numeric_params()?;
17185 mysql_float_md = Some((
17186 u8::try_from(m).unwrap_or(u8::MAX),
17187 u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17188 ));
17189 if ty_ident.eq_ignore_ascii_case("float") {
17190 ColumnTypeName::Real
17191 } else {
17192 ColumnTypeName::Float
17193 }
17194 } else if ty_ident.eq_ignore_ascii_case("float")
17195 && matches!(self.peek(), Token::LParen)
17196 {
17197 // PG words the two bounds differently, and
17198 // parse_paren_size already rejects a zero.
17199 let p = self.parse_paren_size("FLOAT")?;
17200 if p > 53 {
17201 return Err(self.err(String::from(
17202 "precision for type float must be less than 54 bits",
17203 )));
17204 }
17205 if p <= 24 {
17206 ColumnTypeName::Real
17207 } else {
17208 ColumnTypeName::Float
17209 }
17210 } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17211 // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17212 // eight (it is `float8`'s spelling there). SPG used
17213 // PG's for both, so a MySQL FLOAT column silently
17214 // kept more precision than MySQL does — measured,
17215 // 3.14159265358979 comes back as 3.14159 there and
17216 // came back whole here — and reported itself as
17217 // `double` to every reflection.
17218 //
17219 // This is the mirror of the REAL split above: the
17220 // two dialects disagree about which spelling means
17221 // which width, and one of them was already honoured.
17222 ColumnTypeName::Real
17223 } else {
17224 ColumnTypeName::Float
17225 }
17226 }
17227 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17228 "float4" => ColumnTypeName::Real,
17229 "float8" => ColumnTypeName::Float,
17230 "text" => ColumnTypeName::Text,
17231 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17232 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17233 // real MySQL schema and NONE of them existed: the CREATE
17234 // failed outright with `type "blob" does not exist`, so the
17235 // table was never made. The sizes differ only in MySQL's
17236 // maximum length, which SPG does not cap, so they collapse
17237 // onto TEXT and BYTEA the way the unsized spellings do.
17238 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17239 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17240 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17241 // enforce, consumed so the declaration parses.
17242 "varbinary" | "binary" => {
17243 self.consume_optional_paren_size();
17244 ColumnTypeName::Bytes
17245 }
17246 "name" => ColumnTypeName::Name,
17247 "xid" => ColumnTypeName::Xid,
17248 "oid" => ColumnTypeName::Oid,
17249 "xid8" => ColumnTypeName::Xid8,
17250 "bool" | "boolean" => ColumnTypeName::Bool,
17251 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17252 // an unbounded `character varying`, which the arm below has always
17253 // read as text. Only the short spelling demanded a length, so
17254 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17255 // there is — failed on `VARCHAR type requires (N)` while the long
17256 // spelling of the same thing was accepted. The same asymmetry
17257 // round 613 closed on the CAST side, here on the DDL side.
17258 "varchar" => {
17259 if matches!(self.peek(), Token::LParen) {
17260 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17261 } else {
17262 ColumnTypeName::Text
17263 }
17264 }
17265 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17266 // `character` below (SQL standard).
17267 "char" => {
17268 if matches!(self.peek(), Token::LParen) {
17269 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17270 } else {
17271 ColumnTypeName::Char(1)
17272 }
17273 }
17274 // pg_dump's canonical spellings: `character varying(n)` = varchar,
17275 // `character(n)` = char, bare `character` = char(1). Unbounded
17276 // `character varying` maps to text.
17277 "character" => {
17278 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17279 self.advance();
17280 if matches!(self.peek(), Token::LParen) {
17281 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17282 } else {
17283 ColumnTypeName::Text
17284 }
17285 } else if matches!(self.peek(), Token::LParen) {
17286 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17287 } else {
17288 ColumnTypeName::Char(1)
17289 }
17290 }
17291 "vector" => {
17292 let dim = self.parse_paren_size("VECTOR")?;
17293 let encoding = self.parse_optional_vector_encoding()?;
17294 ColumnTypeName::Vector { dim, encoding }
17295 }
17296 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17297 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17298 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17299 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17300 // DECIMAL(10,2))` — how nearly every money column is written,
17301 // in either dialect — was a syntax error and the table was
17302 // never created. `FIXED` is MySQL's alias alone, so it is
17303 // taken only in that dialect.
17304 "numeric" | "decimal" | "dec" => {
17305 let (precision, scale) = self.parse_optional_numeric_params()?;
17306 ColumnTypeName::Numeric(precision, scale)
17307 }
17308 "fixed" if self.mysql_dialect => {
17309 let (precision, scale) = self.parse_optional_numeric_params()?;
17310 ColumnTypeName::Numeric(precision, scale)
17311 }
17312 "date" => ColumnTypeName::Date,
17313 // MySQL's `DATETIME` is the same domain as standard
17314 // `TIMESTAMP` — accept both spellings.
17315 "timestamp" | "datetime" => {
17316 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17317 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17318 // TIME ZONE` clause, so consume it first.
17319 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17320 // (it truncates on write and pads on render), so capture it;
17321 // a bare spelling means precision 0 there. PG stores µs always
17322 // and keeps `None`.
17323 let n = self.take_optional_paren_size();
17324 if self.mysql_dialect {
17325 mysql_fsp = Some(n.unwrap_or(0).min(6));
17326 // v7.39.2 — remember WHICH spelling was written.
17327 // MySQL and MariaDB keep `timestamp` and `datetime`
17328 // apart everywhere a client can read the type back,
17329 // and SPG reported `datetime` for both — so a dump
17330 // and reload silently changed the column's declared
17331 // type, and MySQL's TIMESTAMP is not DATETIME (a
17332 // different range, and UTC conversion on the way in
17333 // and out).
17334 mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17335 }
17336 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17337 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17338 // the full form. SPG canonicalises:
17339 // - WITH TIME ZONE → Timestamptz
17340 // - WITHOUT TIME ZONE → Timestamp
17341 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17342 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17343 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17344 {
17345 self.advance(); // WITH
17346 self.advance(); // TIME
17347 self.advance(); // ZONE
17348 ColumnTypeName::Timestamptz
17349 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17350 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17351 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17352 {
17353 self.advance(); // WITHOUT
17354 self.advance(); // TIME
17355 self.advance(); // ZONE
17356 ColumnTypeName::Timestamp
17357 } else {
17358 // A second `(precision)` cannot legally follow, but the
17359 // old grammar tolerated it; keep that tolerance.
17360 self.consume_optional_paren_size();
17361 ColumnTypeName::Timestamp
17362 }
17363 }
17364 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17365 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17366 // only PG-wire OID differs.
17367 "timestamptz" => {
17368 self.consume_optional_paren_size();
17369 ColumnTypeName::Timestamptz
17370 }
17371 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17372 // validation. We accept the JSONB spelling too because
17373 // most PG clients default to it; SPG doesn't distinguish
17374 // the two (no path-operator perf advantage to model).
17375 "json" => ColumnTypeName::Json,
17376 "jsonb" => ColumnTypeName::Jsonb,
17377 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17378 // surface here. Same storage shape; mapping happens at
17379 // the engine side via the ColumnTypeName → DataType
17380 // resolver. Literal forms are handled at coerce_value
17381 // time so the lexer stays untouched.
17382 "bytea" | "bytes" => ColumnTypeName::Bytes,
17383 // v7.17.0 Phase 7 — PG network address types
17384 // v7.17.0 had a Text-backed fallback here for
17385 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17386 // each to a first-class type; the keywords are
17387 // bound below in the ζ-A block.
17388 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17389 // The actual `to_tsvector` / `@@` / `ts_rank` surface
17390 // arrives in v7.12.1+; the type itself loads here so
17391 // mailrs's `scripts/init-schema.sql` runs unmodified.
17392 "tsvector" => ColumnTypeName::TsVector,
17393 "tsquery" => ColumnTypeName::TsQuery,
17394 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17395 // surface for Django / Rails / Hibernate's default
17396 // PK pattern.
17397 "uuid" => ColumnTypeName::Uuid,
17398 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17399 // Storage = three-field {months, days, micros}, catalog
17400 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17401 // line `INTERVAL` was parser-rejected at CREATE TABLE.
17402 "interval" => {
17403 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17404 // SECOND` and an optional `(p)` precision. SPG stores the full
17405 // {months,days,micros}; consume + ignore the qualifier/precision.
17406 while matches!(self.peek(), Token::To)
17407 || matches!(self.peek(), Token::Ident(s) if matches!(
17408 s.to_ascii_lowercase().as_str(),
17409 "year" | "month" | "day" | "hour" | "minute" | "second"
17410 ))
17411 {
17412 self.advance();
17413 }
17414 self.consume_optional_paren_size();
17415 ColumnTypeName::Interval
17416 }
17417 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17418 // i64 microseconds since 00:00:00. Wire OID 1083.
17419 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17420 "time" => {
17421 // v7.39 (round 424) — MySQL TIME carries a semantic
17422 // fractional-seconds precision, bare meaning 0.
17423 let n = self.take_optional_paren_size();
17424 if self.mysql_dialect {
17425 mysql_fsp = Some(n.unwrap_or(0).min(6));
17426 }
17427 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17428 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17429 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17430 {
17431 self.advance();
17432 self.advance();
17433 self.advance();
17434 ColumnTypeName::TimeTz
17435 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17436 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17437 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17438 {
17439 self.advance();
17440 self.advance();
17441 self.advance();
17442 ColumnTypeName::Time
17443 } else {
17444 ColumnTypeName::Time
17445 }
17446 }
17447 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17448 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17449 "year" => ColumnTypeName::Year,
17450 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17451 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17452 "timetz" => ColumnTypeName::TimeTz,
17453 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17454 // Wire OID 790.
17455 "money" => ColumnTypeName::Money,
17456 // v7.17.0 Phase 3.P0-38 — PG range types.
17457 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17458 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17459 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17460 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17461 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17462 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17463 // v7.37.5 δ — PG 14+ multirange keywords.
17464 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17465 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17466 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17467 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17468 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17469 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17470 // v7.37.5 ε — PG geometry scalar keywords.
17471 "point" => ColumnTypeName::Point,
17472 "lseg" => ColumnTypeName::Lseg,
17473 "path" => ColumnTypeName::Path,
17474 "box" => ColumnTypeName::PgBox,
17475 "polygon" => ColumnTypeName::Polygon,
17476 "line" => ColumnTypeName::Line,
17477 "circle" => ColumnTypeName::Circle,
17478 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17479 "inet" => ColumnTypeName::Inet,
17480 "cidr" => ColumnTypeName::Cidr,
17481 "macaddr" => ColumnTypeName::Macaddr,
17482 "macaddr8" => ColumnTypeName::Macaddr8,
17483 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17484 // width in the value, so the optional `(N)` typmod is accepted and
17485 // ignored (the column stores whatever width it's given).
17486 "bit" => {
17487 let varying = matches!(
17488 self.peek(),
17489 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17490 );
17491 if varying {
17492 self.advance();
17493 }
17494 // v7.39 (round 281) — the length used to be parsed and
17495 // dropped, so `bit(3)` accepted a five-bit string.
17496 let n = if matches!(self.peek(), Token::LParen) {
17497 self.parse_paren_size("BIT")?
17498 } else {
17499 0
17500 };
17501 if varying {
17502 ColumnTypeName::BitVarying(n)
17503 } else {
17504 ColumnTypeName::Bit(n)
17505 }
17506 }
17507 "varbit" => {
17508 let n = if matches!(self.peek(), Token::LParen) {
17509 self.parse_paren_size("VARBIT")?
17510 } else {
17511 0
17512 };
17513 ColumnTypeName::BitVarying(n)
17514 }
17515 "xml" => ColumnTypeName::Xml,
17516 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17517 "hstore" => ColumnTypeName::Hstore,
17518 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17519 // `ENUM('a','b','c')`. Storage is TEXT; the value
17520 // list lands on `inline_enum_variants` for the
17521 // engine to validate INSERT cells against. Empty
17522 // value list is a parse error (matches MySQL).
17523 "enum" => {
17524 // Expect the opening `(`.
17525 if !matches!(self.peek(), Token::LParen) {
17526 return Err(self.err(alloc::format!(
17527 "expected '(' after ENUM, got {:?}",
17528 self.peek()
17529 )));
17530 }
17531 self.advance();
17532 let mut variants: Vec<String> = Vec::new();
17533 loop {
17534 match self.advance() {
17535 Token::String(s) => variants.push(s),
17536 other => {
17537 return Err(self.err(alloc::format!(
17538 "ENUM(...) expects string literal variants, got {other:?}"
17539 )));
17540 }
17541 }
17542 match self.peek() {
17543 Token::Comma => {
17544 self.advance();
17545 continue;
17546 }
17547 Token::RParen => {
17548 self.advance();
17549 break;
17550 }
17551 other => {
17552 return Err(self.err(alloc::format!(
17553 "expected ',' or ')' in ENUM(...), got {other:?}"
17554 )));
17555 }
17556 }
17557 }
17558 if variants.is_empty() {
17559 return Err(self.err("ENUM(...) must declare at least one variant".into()));
17560 }
17561 inline_enum_variants = Some(variants);
17562 // Storage is plain TEXT; the variant list lives on
17563 // the ColumnSchema side.
17564 ColumnTypeName::Text
17565 }
17566 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17567 // `SET('a','b','c')`. Same parse shape as ENUM;
17568 // semantics differ (subset rather than pick-one).
17569 "set" => {
17570 if !matches!(self.peek(), Token::LParen) {
17571 return Err(self.err(alloc::format!(
17572 "expected '(' after SET, got {:?}",
17573 self.peek()
17574 )));
17575 }
17576 self.advance();
17577 let mut variants: Vec<String> = Vec::new();
17578 loop {
17579 match self.advance() {
17580 Token::String(s) => variants.push(s),
17581 other => {
17582 return Err(self.err(alloc::format!(
17583 "SET(...) expects string literal variants, got {other:?}"
17584 )));
17585 }
17586 }
17587 match self.peek() {
17588 Token::Comma => {
17589 self.advance();
17590 continue;
17591 }
17592 Token::RParen => {
17593 self.advance();
17594 break;
17595 }
17596 other => {
17597 return Err(self.err(alloc::format!(
17598 "expected ',' or ')' in SET(...), got {other:?}"
17599 )));
17600 }
17601 }
17602 }
17603 if variants.is_empty() {
17604 return Err(self.err("SET(...) must declare at least one variant".into()));
17605 }
17606 inline_set_variants = Some(variants);
17607 ColumnTypeName::Text
17608 }
17609 _other => {
17610 // v7.17.0 Phase 1.4 — unknown ident → defer
17611 // resolution to the engine. Stored as Text in
17612 // ColumnTypeName + the original name carried as
17613 // `user_type_ref` so CREATE TABLE can look up
17614 // user-defined enum / domain types.
17615 user_type_ref = Some(ty_ident.clone());
17616 ColumnTypeName::Text
17617 }
17618 };
17619 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17620 // right after the type keyword. Pre-4.4 SPG consumed +
17621 // discarded the keyword, leaving a customer column
17622 // declared `id INT UNSIGNED NOT NULL` silently accepting
17623 // negative values — a Tier-A correctness drift where
17624 // application invariants (auto-increment-IDs never
17625 // negative) silently broke on cutover. Now: capture as
17626 // a column flag, persist on the schema, enforce at
17627 // INSERT / UPDATE time.
17628 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17629 {
17630 self.advance();
17631 true
17632 } else {
17633 false
17634 };
17635 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17636 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17637 // stores text as UTF-8 always so CHARACTER SET is still a
17638 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17639 // name: it gets classified into a `Collation` variant the
17640 // engine consults at WHERE-eval time. PG `default` /
17641 // `pg_catalog.default` / `C` / `POSIX` collations all
17642 // resolve to `Binary` (the prior behaviour); `_ci` /
17643 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17644 // The schema-qualifier form (`pg_catalog.default`) lexes
17645 // as `Ident '.' Ident` — peek for the `.` and consume both
17646 // halves so it's treated as one collation name. PG's
17647 // `IDENT.IDENT` collation form (which can appear here) is
17648 // resolved by Collation::from_collation_name on the bare
17649 // identifier after the dot.
17650 let mut collation = Collation::Binary;
17651 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17652 // clause was written. The engine needs this to tell an explicit
17653 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17654 // clause at all: both resolve to `Collation::Binary`, but under the
17655 // MySQL dialect the latter takes the folding default collation.
17656 let mut collation_explicit = false;
17657 let mut collation_name: Option<alloc::string::String> = None;
17658 loop {
17659 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17660 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17661 {
17662 self.advance(); // CHARACTER
17663 self.advance(); // SET
17664 if matches!(
17665 self.peek(),
17666 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17667 ) {
17668 self.advance();
17669 }
17670 continue;
17671 }
17672 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17673 self.advance(); // COLLATE
17674 // Accept Ident / QuotedIdent / String AND the
17675 // keyword-tokenised `Default` (PG `pg_catalog.default`
17676 // and bare `DEFAULT` collation names — `default` is a
17677 // reserved word so the lexer hands back Token::Default
17678 // not Token::Ident).
17679 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17680 match this.peek().clone() {
17681 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17682 this.advance();
17683 Some(s)
17684 }
17685 Token::Default => {
17686 this.advance();
17687 Some(alloc::string::String::from("default"))
17688 }
17689 _ => None,
17690 }
17691 };
17692 let raw = if let Some(head) = read_collation_atom(self) {
17693 // Schema-qualified PG form: `pg_catalog.default`.
17694 if matches!(self.peek(), Token::Dot) {
17695 self.advance();
17696 let tail = read_collation_atom(self).unwrap_or_default();
17697 alloc::format!("{head}.{tail}")
17698 } else {
17699 head
17700 }
17701 } else {
17702 alloc::string::String::new()
17703 };
17704 if !raw.is_empty() {
17705 collation_explicit = true;
17706 // v7.39 (round 676) — keep the name too. The enum below
17707 // folds C / POSIX / en_US / default into one value, and
17708 // `pg_attribute.attcollation` has to tell them apart.
17709 // The schema qualifier goes: PG's `pg_catalog.default`
17710 // and a bare `default` name the same collation.
17711 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17712 // encoding suffix. Round 676 used `rsplit('.')` for
17713 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17714 // PG writes `pg_catalog.default` (qualifier) and
17715 // `en_US.utf8` (locale + encoding) with the same
17716 // separator. Only `pg_catalog.` is a qualifier, and it
17717 // is the only one PG's own dumps emit.
17718 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17719 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17720 collation_name = Some(alloc::string::String::from(bare));
17721 let parsed = Collation::from_collation_name(&raw);
17722 // Last COLLATE clause wins, but `Binary` from a
17723 // bare keyword like `default` should not
17724 // silently downgrade a stronger one set earlier
17725 // on the same column. v7.17 only ships one
17726 // non-Binary variant so a simple OR is enough.
17727 if parsed != Collation::Binary {
17728 collation = parsed;
17729 }
17730 }
17731 continue;
17732 }
17733 break;
17734 }
17735 // v7.10.10 — postfix `[]` widens the base type to its array
17736 // type. PG accepts `TYPE[]` after any base type and so does
17737 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17738 // all through; the old "only TEXT[]" note was stale).
17739 if matches!(self.peek(), Token::LBracket) {
17740 self.advance();
17741 if !matches!(self.peek(), Token::RBracket) {
17742 return Err(self.err(alloc::format!(
17743 "TEXT[] takes no dimension; got {:?}",
17744 self.peek()
17745 )));
17746 }
17747 self.advance();
17748 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17749 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17750 // still error here.
17751 ty = match ty {
17752 ColumnTypeName::Text => ColumnTypeName::TextArray,
17753 ColumnTypeName::Int => ColumnTypeName::IntArray,
17754 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17755 // v7.40.0 — `oid[]`. Everything downstream of the
17756 // parser already handled `DataType::OidArray`; this
17757 // arm is the whole of what was missing.
17758 ColumnTypeName::Oid => ColumnTypeName::OidArray,
17759 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17760 // `[]` grammar. Wire OID 1187.
17761 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17762 // v7.37.5 γ — full PG array-of-scalar family.
17763 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17764 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17765 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17766 // NUMERIC(p, s) loses its precision params at the
17767 // array level (matches PG: `NUMERIC[]` is untyped,
17768 // per-element precision flows through values).
17769 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17770 ColumnTypeName::Date => ColumnTypeName::DateArray,
17771 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17772 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17773 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17774 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17775 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17776 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17777 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17778 // the array level (matches PG semantics where the
17779 // element precision is per-row, not column-wide).
17780 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17781 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17782 // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17783 // precision the same way NUMERIC[] does.
17784 ColumnTypeName::Real => ColumnTypeName::RealArray,
17785 ColumnTypeName::Time => ColumnTypeName::TimeArray,
17786 ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17787 ColumnTypeName::Inet => ColumnTypeName::InetArray,
17788 ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17789 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17790 // follow-up.
17791 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17792 other => {
17793 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17794 }
17795 };
17796 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17797 // for INT/TEXT/BIGINT. Anything else is an error.
17798 if matches!(self.peek(), Token::LBracket) {
17799 self.advance();
17800 if !matches!(self.peek(), Token::RBracket) {
17801 return Err(self.err(alloc::format!(
17802 "TYPE[][] second dimension takes no size; got {:?}",
17803 self.peek()
17804 )));
17805 }
17806 self.advance();
17807 ty = match ty {
17808 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17809 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17810 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17811 // v7.39 (read01 round 75) — bool[][].
17812 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17813 other => {
17814 return Err(self.err(alloc::format!(
17815 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17816 TEXT[][] only; got {other:?}"
17817 )));
17818 }
17819 };
17820 }
17821 }
17822 Ok((
17823 ty,
17824 implied_auto_increment,
17825 implied_not_null,
17826 user_type_ref,
17827 collation,
17828 collation_explicit,
17829 collation_name,
17830 is_unsigned,
17831 inline_enum_variants,
17832 inline_set_variants,
17833 mysql_int_width,
17834 mysql_fsp,
17835 mysql_declared_timestamp,
17836 mysql_float_md,
17837 ))
17838 }
17839
17840 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17841 // v7.20 — PG reserves the table-constraint keywords, so a
17842 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17843 // malformed constraint clause (e.g. `UNIQUE a` missing its
17844 // parens), not a column named "unique". Since v7.17's
17845 // unknown-type leniency (`user_type_ref`) such a clause
17846 // would otherwise parse as a column with a user-defined
17847 // type — silently accepting invalid DDL. Quoted
17848 // identifiers ("unique" / `unique`) remain valid names.
17849 if let Token::Ident(s) = self.peek()
17850 && [
17851 "unique",
17852 "primary",
17853 "foreign",
17854 "constraint",
17855 "check",
17856 "references",
17857 "exclude",
17858 ]
17859 .iter()
17860 .any(|kw| s.eq_ignore_ascii_case(kw))
17861 {
17862 return Err(self.err(alloc::format!(
17863 "unexpected reserved keyword '{s}' at start of column definition \
17864 (malformed table constraint?)"
17865 )));
17866 }
17867 let name_tok = self.pos;
17868 let name = self.expect_ident_like()?;
17869 // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17870 // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17871 // information_schema, and in SHOW CREATE (measured). SPG folded
17872 // an unquoted name, so a table restored from a dump reported
17873 // names the application had never written.
17874 //
17875 // The written form comes back from the source span, which only
17876 // the MySQL dialect keeps. The span runs to the START of the
17877 // next token, so a comment or unusual spacing between them
17878 // arrives with it — hence the check that what came back is the
17879 // same identifier. It is not decoration: without it,
17880 // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17881 // `MyCol /* c */`.
17882 let name = self
17883 .source_span(name_tok, name_tok)
17884 .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17885 .filter(|raw| raw.eq_ignore_ascii_case(&name))
17886 .map_or(name, alloc::string::String::from);
17887 let (
17888 ty,
17889 implied_auto_increment,
17890 implied_not_null,
17891 user_type_ref,
17892 collation,
17893 collation_explicit,
17894 collation_name,
17895 is_unsigned,
17896 inline_enum_variants,
17897 inline_set_variants,
17898 mysql_int_width,
17899 mysql_fsp,
17900 mysql_declared_timestamp,
17901 mysql_float_md,
17902 ) = self.parse_type_with_implied_flags()?;
17903 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17904 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17905 // each at most once.
17906 let mut default: Option<Expr> = None;
17907 let mut nullable = !implied_not_null;
17908 let mut nullability_seen = implied_not_null;
17909 let mut auto_increment = implied_auto_increment;
17910 let mut is_primary_key = false;
17911 let mut is_unique = false;
17912 let mut unique_nulls_not_distinct = false;
17913 let mut constraint_deferrable = false;
17914 let mut constraint_initially_deferred = false;
17915 let mut check: Option<Expr> = None;
17916 let mut on_update_runtime: Option<Expr> = None;
17917 let mut generated_stored_expr: Option<Box<Expr>> = None;
17918 let mut identity_always = false;
17919 loop {
17920 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17921 // not-null constraints by name and pg_dump emits them
17922 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17923 // NOT NULL`. Accept and discard the name; whatever
17924 // constraint follows is parsed by the arms below.
17925 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17926 // v7.39 (round 308, V29) — a name on an inline
17927 // REFERENCES belongs to the FOREIGN KEY, and the caller
17928 // (`parse_column_def_with_fk`) is what builds it, so
17929 // leave the whole clause for it. Dropping the name here
17930 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17931 // as the synthesised `c_pid_fkey` — which then could
17932 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17933 // `advance()` takes tokens by `mem::replace`, so there
17934 // is no rewinding once consumed.
17935 if matches!(
17936 self.tokens.get(self.pos + 2),
17937 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17938 ) {
17939 break;
17940 }
17941 self.advance();
17942 let _name = self.expect_ident_like()?;
17943 continue;
17944 }
17945 // v7.39 (round 379) — MySQL's SHORT generated-column form
17946 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17947 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17948 // below), but hand-written schemas and app migrations use this.
17949 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17950 // SPG computes-and-stores either way, like the long form.
17951 if matches!(self.peek(), Token::As) {
17952 self.advance();
17953 if !matches!(self.peek(), Token::LParen) {
17954 return Err(self.err(alloc::format!(
17955 "expected '(' after AS in a generated column, got {:?}",
17956 self.peek()
17957 )));
17958 }
17959 self.advance();
17960 let expr = self.parse_expr(0)?;
17961 if !matches!(self.peek(), Token::RParen) {
17962 return Err(self.err(alloc::format!(
17963 "expected ')' after AS (<expr>), got {:?}",
17964 self.peek()
17965 )));
17966 }
17967 self.advance();
17968 if matches!(self.peek(), Token::Ident(s)
17969 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17970 {
17971 self.advance();
17972 }
17973 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17974 continue;
17975 }
17976 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17977 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17978 // the modern replacement for SERIAL in hand-written
17979 // schemas). Both flavours map onto the auto-increment
17980 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17981 // ALWAYS's reject-explicit-values nuance is documented
17982 // leniency. Generated EXPRESSION columns
17983 // (`AS (expr) STORED`) are not supported: error loudly
17984 // instead of silently storing NULLs.
17985 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17986 self.advance();
17987 let mut saw_generated_always = false;
17988 match self.peek().clone() {
17989 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17990 self.advance();
17991 saw_generated_always = true;
17992 }
17993 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17994 self.advance();
17995 if !matches!(self.peek(), Token::Default) {
17996 return Err(self.err(alloc::format!(
17997 "expected DEFAULT after GENERATED BY, got {:?}",
17998 self.peek()
17999 )));
18000 }
18001 self.advance();
18002 }
18003 other => {
18004 return Err(self.err(alloc::format!(
18005 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
18006 )));
18007 }
18008 }
18009 if !matches!(self.peek(), Token::As) {
18010 return Err(self.err(alloc::format!(
18011 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
18012 self.peek()
18013 )));
18014 }
18015 self.advance();
18016 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
18017 // ( <expr> ) STORED` stored computed-column. The
18018 // expression is captured for the engine to recompute
18019 // on every INSERT / UPDATE. v7.37.7 accepts the
18020 // STORED keyword only; PG also has VIRTUAL, which
18021 // v7.37.7 carves out (sentori only uses STORED).
18022 if matches!(self.peek(), Token::LParen) {
18023 self.advance();
18024 let expr = self.parse_expr(0)?;
18025 if !matches!(self.peek(), Token::RParen) {
18026 return Err(self.err(alloc::format!(
18027 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
18028 self.peek()
18029 )));
18030 }
18031 self.advance();
18032 let stored = match self.peek() {
18033 Token::Ident(s) | Token::QuotedIdent(s)
18034 if s.eq_ignore_ascii_case("stored") =>
18035 {
18036 self.advance();
18037 true
18038 }
18039 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
18040 // generated columns. SPG computes them on write and
18041 // persists like STORED; the two are observably
18042 // identical for query results (the value, recompute
18043 // on base-column change, and NOT NULL enforcement all
18044 // match), so a PG 18 schema/dump using VIRTUAL loads
18045 // and behaves correctly. The compute-on-read storage
18046 // saving is an invisible internal difference.
18047 Token::Ident(s) | Token::QuotedIdent(s)
18048 if s.eq_ignore_ascii_case("virtual") =>
18049 {
18050 self.advance();
18051 false
18052 }
18053 other => {
18054 return Err(self.err(alloc::format!(
18055 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18056 got {other:?}"
18057 )));
18058 }
18059 };
18060 let _ = stored; // STORED / VIRTUAL both compute-and-store.
18061 generated_stored_expr = Some(Box::new(expr));
18062 continue;
18063 }
18064 self.expect_keyword_ident("identity")?;
18065 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18066 // consume the balanced parens and discard (SPG's
18067 // auto-increment is max+1-scan based).
18068 if matches!(self.peek(), Token::LParen) {
18069 let mut depth = 0usize;
18070 loop {
18071 match self.advance() {
18072 Token::LParen => depth += 1,
18073 Token::RParen => {
18074 depth -= 1;
18075 if depth == 0 {
18076 break;
18077 }
18078 }
18079 Token::Eof => {
18080 return Err(self.err(
18081 "unterminated sequence-options parens after IDENTITY".into(),
18082 ));
18083 }
18084 _ => {}
18085 }
18086 }
18087 }
18088 auto_increment = true;
18089 // v7.38 (read01) — remember the ALWAYS flavour so the engine
18090 // can reject explicit non-DEFAULT INSERT values (unless
18091 // OVERRIDING SYSTEM VALUE) the way PG does.
18092 identity_always = saw_generated_always;
18093 // PG identity columns are implicitly NOT NULL.
18094 nullable = false;
18095 continue;
18096 }
18097 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18098 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18099 // is accepted today. The "ON" token is an Ident
18100 // (not reserved) — peek before consuming.
18101 if matches!(self.peek(), Token::On)
18102 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18103 {
18104 self.advance(); // ON
18105 self.advance(); // update
18106 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18107 let next = self.peek().clone();
18108 match next {
18109 Token::Ident(s) | Token::QuotedIdent(s)
18110 if s.eq_ignore_ascii_case("current_timestamp") =>
18111 {
18112 self.advance();
18113 // Optional `(N)` precision.
18114 if matches!(self.peek(), Token::LParen) {
18115 self.advance();
18116 if !matches!(self.peek(), Token::Integer(_)) {
18117 return Err(self.err(alloc::format!(
18118 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18119 self.peek()
18120 )));
18121 }
18122 self.advance();
18123 if !matches!(self.peek(), Token::RParen) {
18124 return Err(self.err(alloc::format!(
18125 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18126 self.peek()
18127 )));
18128 }
18129 self.advance();
18130 }
18131 on_update_runtime = Some(Expr::FunctionCall {
18132 name: "now".into(),
18133 args: Vec::new(),
18134 });
18135 continue;
18136 }
18137 other => {
18138 return Err(self.err(alloc::format!(
18139 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18140 )));
18141 }
18142 }
18143 }
18144 if matches!(self.peek(), Token::Default) {
18145 if default.is_some() {
18146 return Err(self.err("DEFAULT specified twice".into()));
18147 }
18148 self.advance();
18149 default = Some(self.parse_expr(0)?);
18150 continue;
18151 }
18152 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18153 // token with NOT NULL and sits EARLIER in the loop than the
18154 // deferrability arm, so without the lookahead it was reported as
18155 // "NOT NULL specified twice" (or "expected NULL after NOT").
18156 if matches!(self.peek(), Token::Not)
18157 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18158 {
18159 // NOT DEFERRABLE — explicit immediate; nothing to carry.
18160 self.consume_optional_deferrable_clauses()?;
18161 continue;
18162 }
18163 if matches!(self.peek(), Token::Not) {
18164 if nullability_seen {
18165 return Err(self.err("NOT NULL specified twice".into()));
18166 }
18167 self.advance();
18168 if !matches!(self.peek(), Token::Null) {
18169 return Err(self.err(format!(
18170 "expected NULL after NOT in column def, got {:?}",
18171 self.peek()
18172 )));
18173 }
18174 self.advance();
18175 nullable = false;
18176 nullability_seen = true;
18177 continue;
18178 }
18179 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18180 // "this column is nullable" marker (the default in
18181 // standard SQL anyway). mysqldump emits it routinely
18182 // (`col TYPE NULL DEFAULT NULL` for nullable
18183 // timestamps etc). Accept + no-op.
18184 if matches!(self.peek(), Token::Null) {
18185 if nullability_seen && !nullable {
18186 // v7.39 (round 761, F31 tranche 2 #31) — PG's
18187 // sentence, PG18-measured (the table name is the
18188 // caller's; the column half is exact).
18189 return Err(self.err(alloc::format!(
18190 "conflicting NULL/NOT NULL declarations for column \"{name}\""
18191 )));
18192 }
18193 self.advance();
18194 nullable = true;
18195 nullability_seen = true;
18196 continue;
18197 }
18198 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18199 // arrives as a bare Ident. Match either, case-insensitive.
18200 if let Token::Ident(s) = self.peek()
18201 && (s.eq_ignore_ascii_case("auto_increment")
18202 || s.eq_ignore_ascii_case("autoincrement"))
18203 {
18204 if auto_increment {
18205 return Err(self.err("AUTO_INCREMENT specified twice".into()));
18206 }
18207 self.advance();
18208 auto_increment = true;
18209 continue;
18210 }
18211 // v7.9.13 — inline `PRIMARY KEY` column constraint
18212 // (mailrs F1). Implies `NOT NULL`. The engine creates
18213 // a BTree index for the PK column at CREATE TABLE time
18214 // so FK parent-side index lookups resolve.
18215 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18216 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18217 // spelling was a parse error, so a pg_dump carrying one stopped
18218 // mid-restore. The clauses are consumed by the same helper the FK
18219 // path has used since round 288 and recorded nowhere: SPG enforces
18220 // the constraint IMMEDIATELY either way, which fails earlier than
18221 // PG inside a transaction that violates-then-repairs — a refusal,
18222 // not a wrong answer. True deferral is the open remainder of F08.
18223 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18224 || (matches!(self.peek(), Token::Not)
18225 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18226 {
18227 // v7.39 (round 711) — CARRIED now (the storing half of
18228 // F08); round 621 only consumed.
18229 let (d, idef) = self.consume_deferrable_clauses_timed()?;
18230 constraint_deferrable |= d;
18231 constraint_initially_deferred |= idef;
18232 continue;
18233 }
18234 if let Token::Ident(s) = self.peek()
18235 && s.eq_ignore_ascii_case("primary")
18236 {
18237 if is_primary_key {
18238 return Err(self.err("PRIMARY KEY specified twice".into()));
18239 }
18240 // Peek-ahead for the required `KEY` token.
18241 let next = self.tokens.get(self.pos + 1);
18242 let next_is_key = matches!(
18243 next,
18244 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18245 );
18246 if !next_is_key {
18247 return Err(self.err(format!(
18248 "expected KEY after PRIMARY in column def, got {:?}",
18249 next
18250 )));
18251 }
18252 self.advance(); // PRIMARY
18253 self.advance(); // KEY
18254 is_primary_key = true;
18255 if nullability_seen && nullable {
18256 return Err(self.err(
18257 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18258 ));
18259 }
18260 nullable = false;
18261 nullability_seen = true;
18262 continue;
18263 }
18264 // v7.13.0 — inline `UNIQUE` column constraint
18265 // (mailrs round-5 G2). Fold into a single-column
18266 // table-level UNIQUE at CREATE TABLE post-process time.
18267 if let Token::Ident(s) = self.peek()
18268 && s.eq_ignore_ascii_case("unique")
18269 {
18270 if is_unique {
18271 return Err(self.err("UNIQUE specified twice".into()));
18272 }
18273 self.advance();
18274 is_unique = true;
18275 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18276 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18277 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18278 let n1 = self.tokens.get(self.pos + 1);
18279 let n2 = self.tokens.get(self.pos + 2);
18280 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18281 self.advance(); // NULLS
18282 self.advance(); // NOT
18283 self.advance(); // DISTINCT
18284 unique_nulls_not_distinct = true;
18285 } else if matches!(n1, Some(Token::Distinct)) {
18286 self.advance(); // NULLS
18287 self.advance(); // DISTINCT
18288 }
18289 }
18290 continue;
18291 }
18292 // v7.13.0 — inline `CHECK (<expr>)` column constraint
18293 // (mailrs round-5 G3). PG semantics: column-level
18294 // CHECK is equivalent to a table-level CHECK. Multiple
18295 // inline CHECKs on the same column AND together.
18296 if let Token::Ident(s) = self.peek()
18297 && s.eq_ignore_ascii_case("check")
18298 {
18299 self.advance();
18300 if !matches!(self.peek(), Token::LParen) {
18301 return Err(self.err(alloc::format!(
18302 "expected '(' after CHECK in column def, got {:?}",
18303 self.peek()
18304 )));
18305 }
18306 self.advance();
18307 let pred = self.parse_expr(0)?;
18308 if !matches!(self.peek(), Token::RParen) {
18309 return Err(self.err(alloc::format!(
18310 "expected ')' to close CHECK predicate, got {:?}",
18311 self.peek()
18312 )));
18313 }
18314 self.advance();
18315 check = Some(match check.take() {
18316 Some(prev) => Expr::Binary {
18317 op: BinOp::And,
18318 lhs: Box::new(prev),
18319 rhs: Box::new(pred),
18320 },
18321 None => pred,
18322 });
18323 continue;
18324 }
18325 break;
18326 }
18327 Ok(ColumnDef {
18328 name,
18329 ty,
18330 nullable,
18331 default,
18332 auto_increment,
18333 is_primary_key,
18334 is_unique,
18335 unique_nulls_not_distinct,
18336 constraint_deferrable,
18337 constraint_initially_deferred,
18338 check,
18339 user_type_ref,
18340 on_update_runtime,
18341 collation,
18342 collation_explicit,
18343 collation_name,
18344 is_unsigned,
18345 inline_enum_variants,
18346 inline_set_variants,
18347 generated_stored_expr,
18348 identity_always,
18349 mysql_int_width,
18350 mysql_fsp,
18351 mysql_declared_timestamp,
18352 mysql_float_md,
18353 })
18354 }
18355
18356 /// `NUMERIC` may appear without parameters, with one (precision
18357 /// only, scale=0), or with both. Returns `(precision, scale)` with
18358 /// 0 = unspecified for the bare form.
18359 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18360 if !matches!(self.peek(), Token::LParen) {
18361 // Bare `NUMERIC` — PG treats this as "unlimited precision";
18362 // we surface it as precision=0 to mean "unconstrained" so
18363 // the engine doesn't need a separate variant.
18364 return Ok((0, 0));
18365 }
18366 self.advance();
18367 // v7.39 (round 272) — PG's declared precision runs to 1000, and
18368 // it words the out-of-range case with the value it saw. SPG
18369 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18370 // accepts failed to parse at all; values wider than i128 are
18371 // carried by the arbitrary-precision form.
18372 let precision = match self.advance() {
18373 Token::Integer(n) if (1..=1000).contains(&n) => {
18374 u16::try_from(n).expect("range-checked")
18375 }
18376 Token::Integer(n) => {
18377 return Err(ParseError {
18378 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18379 token_pos: self.consumed_pos(),
18380 });
18381 }
18382 other => {
18383 return Err(ParseError {
18384 message: format!(
18385 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18386 ),
18387 token_pos: self.consumed_pos(),
18388 });
18389 }
18390 };
18391 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18392 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18393 // then overflows). A negative scale rounds to tens / hundreds / …
18394 let scale = if matches!(self.peek(), Token::Comma) {
18395 self.advance();
18396 let neg = if matches!(self.peek(), Token::Minus) {
18397 self.advance();
18398 true
18399 } else {
18400 false
18401 };
18402 match self.advance() {
18403 Token::Integer(n) => {
18404 let signed = if neg { -n } else { n };
18405 if !(-1000..=1000).contains(&signed) {
18406 return Err(ParseError {
18407 message: format!(
18408 "NUMERIC scale {signed} must be between -1000 and 1000"
18409 ),
18410 token_pos: self.consumed_pos(),
18411 });
18412 }
18413 i16::try_from(signed).expect("range-checked")
18414 }
18415 other => {
18416 return Err(ParseError {
18417 message: format!("NUMERIC scale must be an integer, got {other:?}"),
18418 token_pos: self.consumed_pos(),
18419 });
18420 }
18421 }
18422 } else {
18423 0
18424 };
18425 if !matches!(self.peek(), Token::RParen) {
18426 return Err(self.err(format!(
18427 "expected ')' to close NUMERIC params, got {:?}",
18428 self.peek()
18429 )));
18430 }
18431 self.advance();
18432 Ok((precision, scale))
18433 }
18434
18435 /// Parse `(N)` where `N` is a positive integer literal — used by the
18436 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18437 /// for the error message.
18438 /// v6.0.1: parse the optional `USING <encoding>` clause that
18439 /// follows `VECTOR(N)` in a column definition. Missing clause
18440 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18441 /// ident → `ParseError` listing the encodings recognised today.
18442 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18443 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18444 return Ok(VecEncoding::F32);
18445 }
18446 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18447 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18448 // consume the token when the very next token is a known
18449 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18450 // `USING` for the caller — it's the rewrite-expression form.
18451 let n1 = self.tokens.get(self.pos + 1);
18452 let next_is_encoding = matches!(
18453 n1,
18454 Some(Token::Ident(s))
18455 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18456 );
18457 if !next_is_encoding {
18458 return Ok(VecEncoding::F32);
18459 }
18460 self.advance();
18461 let enc_ident = match self.advance() {
18462 Token::Ident(s) => s,
18463 other => {
18464 return Err(self.err(format!(
18465 "expected vector encoding after USING, got {other:?}"
18466 )));
18467 }
18468 };
18469 match enc_ident.to_ascii_lowercase().as_str() {
18470 "sq8" => Ok(VecEncoding::Sq8),
18471 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18472 // binary16 per-element storage.
18473 "half" => Ok(VecEncoding::F16),
18474 other => Err(self.err(format!(
18475 "unknown vector encoding {other:?}; supported: SQ8, HALF"
18476 ))),
18477 }
18478 }
18479
18480 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18481 /// without consuming it. Returns `Some(N)` when the next
18482 /// tokens are `( <int> )`; None otherwise. Used by the
18483 /// TINYINT classifier to decide whether to map to Bool or
18484 /// SmallInt.
18485 fn peek_optional_paren_size_value(&self) -> Option<i64> {
18486 if !matches!(self.peek(), Token::LParen) {
18487 return None;
18488 }
18489 let next = self.tokens.get(self.pos + 1)?;
18490 let n = match next {
18491 Token::Integer(n) => *n,
18492 _ => return None,
18493 };
18494 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18495 return None;
18496 }
18497 Some(n)
18498 }
18499
18500 /// v7.14.0 — consume an optional MySQL display-width
18501 /// parenthesised number after an integer type, returning
18502 /// nothing. `TINYINT(1)` etc.
18503 /// v7.39 (round 360) — does the parenthesised group ahead contain a
18504 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18505 fn peek_paren_has_comma(&self) -> bool {
18506 let mut i = self.pos + 1;
18507 let mut depth = 1usize;
18508 while depth > 0 {
18509 match self.tokens.get(i) {
18510 Some(Token::LParen) => depth += 1,
18511 Some(Token::RParen) => depth -= 1,
18512 Some(Token::Comma) if depth == 1 => return true,
18513 None | Some(Token::Eof) => return false,
18514 _ => {}
18515 }
18516 i += 1;
18517 }
18518 false
18519 }
18520
18521 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18522 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18523 /// fractional-seconds precision that drives write truncation and render
18524 /// padding, where `consume_optional_paren_size` throws it away.
18525 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18526 fn take_optional_paren_size(&mut self) -> Option<u8> {
18527 let Some(Token::Integer(n)) = self
18528 .tokens
18529 .get(self.pos + 1)
18530 .filter(|_| matches!(self.peek(), Token::LParen))
18531 .cloned()
18532 else {
18533 self.consume_optional_paren_size();
18534 return None;
18535 };
18536 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18537 self.consume_optional_paren_size();
18538 return None;
18539 }
18540 self.consume_optional_paren_size();
18541 u8::try_from(n).ok()
18542 }
18543
18544 fn consume_optional_paren_size(&mut self) {
18545 if !matches!(self.peek(), Token::LParen) {
18546 return;
18547 }
18548 self.advance();
18549 // Skip until matching RParen (allow nested or any tokens).
18550 let mut depth = 1usize;
18551 while depth > 0 {
18552 match self.peek() {
18553 Token::LParen => depth += 1,
18554 Token::RParen => depth -= 1,
18555 Token::Eof => return,
18556 _ => {}
18557 }
18558 self.advance();
18559 }
18560 }
18561
18562 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18563 if !matches!(self.peek(), Token::LParen) {
18564 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18565 }
18566 self.advance();
18567 let n = match self.advance() {
18568 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18569 message: format!("{label} size too large: {n}"),
18570 token_pos: self.consumed_pos(),
18571 })?,
18572 other => {
18573 return Err(ParseError {
18574 message: format!("expected positive integer {label} size, got {other:?}"),
18575 token_pos: self.consumed_pos(),
18576 });
18577 }
18578 };
18579 if !matches!(self.peek(), Token::RParen) {
18580 return Err(self.err(format!(
18581 "expected ')' after {label} size, got {:?}",
18582 self.peek()
18583 )));
18584 }
18585 self.advance();
18586 Ok(n)
18587 }
18588
18589 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18590 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18591 /// key, like MySQL) whose action skips conflicting rows.
18592 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18593 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18594 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18595 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18596 /// common bulk-upsert spellings —
18597 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18598 /// REPLACE INTO t SELECT …
18599 /// — were a parse error / a duplicate-key failure respectively.
18600 ///
18601 /// Precedence: an explicitly written clause beats a statement-level flag.
18602 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18603 /// implicit `REPLACE` and `IGNORE` lowerings.
18604 fn parse_insert_conflict_clause(
18605 &mut self,
18606 replace: bool,
18607 ignore: bool,
18608 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18609 if let Some(c) = self.parse_optional_on_duplicate_key()? {
18610 return Ok(Some(c));
18611 }
18612 if let Some(c) = self.parse_optional_on_conflict()? {
18613 return Ok(Some(c));
18614 }
18615 if replace {
18616 // REPLACE INTO = delete-then-insert, which PG spells as
18617 // `ON CONFLICT DO UPDATE SET` over every column; the engine
18618 // reads an empty assignment list as "take the incoming row".
18619 return Ok(Some(crate::ast::OnConflictClause {
18620 target_columns: Vec::new(),
18621 index_where: None,
18622 constraint_name: None,
18623 mysql_lowered: true,
18624 action: crate::ast::OnConflictAction::Update {
18625 assignments: Vec::new(),
18626 where_: None,
18627 },
18628 }));
18629 }
18630 if ignore {
18631 return Ok(Some(Self::insert_ignore_clause()));
18632 }
18633 Ok(None)
18634 }
18635
18636 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18637 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18638 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18639 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18640 fn parse_optional_on_duplicate_key(
18641 &mut self,
18642 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18643 if !(matches!(self.peek(), Token::On)
18644 && matches!(self.tokens.get(self.pos + 1),
18645 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18646 {
18647 return Ok(None);
18648 }
18649 self.advance(); // ON
18650 self.advance(); // DUPLICATE
18651 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18652 return Err(self.err(format!(
18653 "expected KEY after ON DUPLICATE, got {:?}",
18654 self.peek()
18655 )));
18656 }
18657 self.advance();
18658 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18659 return Err(self.err(format!(
18660 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18661 self.peek()
18662 )));
18663 }
18664 self.advance();
18665 let mut assignments: Vec<(String, Expr)> = Vec::new();
18666 loop {
18667 let col = self.expect_ident_like()?;
18668 if !matches!(self.peek(), Token::Eq) {
18669 return Err(self.err(format!(
18670 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18671 self.peek()
18672 )));
18673 }
18674 self.advance();
18675 let mut expr = self.parse_expr(0)?;
18676 Self::rewrite_mysql_values_refs(&mut expr);
18677 assignments.push((col, expr));
18678 if matches!(self.peek(), Token::Comma) {
18679 self.advance();
18680 continue;
18681 }
18682 break;
18683 }
18684 Ok(Some(crate::ast::OnConflictClause {
18685 target_columns: Vec::new(),
18686 index_where: None,
18687 constraint_name: None,
18688 mysql_lowered: true,
18689 action: crate::ast::OnConflictAction::Update {
18690 assignments,
18691 where_: None,
18692 },
18693 }))
18694 }
18695
18696 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18697 crate::ast::OnConflictClause {
18698 target_columns: Vec::new(),
18699 index_where: None,
18700 constraint_name: None,
18701 mysql_lowered: true,
18702 action: crate::ast::OnConflictAction::Nothing,
18703 }
18704 }
18705
18706 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18707 debug_assert!(
18708 matches!(self.peek(), Token::Insert)
18709 || (replace
18710 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18711 );
18712 self.advance();
18713 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18714 // would raise a duplicate-key error instead of failing the statement,
18715 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18716 // plain ident to the lexer; only the MySQL dialect accepts it here.
18717 let ignore = self.mysql_dialect
18718 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18719 if ignore {
18720 self.advance();
18721 }
18722 if !matches!(self.peek(), Token::Into) {
18723 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18724 }
18725 self.advance();
18726 let table = self.expect_ident_like()?;
18727 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18728 // grammar requires the AS keyword here (a bare identifier would be
18729 // ambiguous with a column list). The alias is what the ON CONFLICT
18730 // DO UPDATE expressions refer to the target row by.
18731 let alias = if matches!(self.peek(), Token::As) {
18732 self.advance();
18733 Some(self.expect_ident_like()?)
18734 } else {
18735 None
18736 };
18737 // v7.39 (round 428) — MySQL's SET-form INSERT:
18738 // INSERT INTO t SET a = 1, b = 'x'
18739 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18740 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18741 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18742 // measured). So it lowers to the column list + one VALUES row and
18743 // rejoins the ordinary path, which already handles every one of
18744 // those. PG has no such spelling, hence the dialect gate.
18745 if self.mysql_dialect
18746 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18747 {
18748 self.advance(); // SET
18749 let mut names = Vec::new();
18750 let mut values = Vec::new();
18751 loop {
18752 names.push(self.expect_ident_like()?);
18753 if !matches!(self.peek(), Token::Eq) {
18754 return Err(self.err(alloc::format!(
18755 "expected '=' in INSERT … SET, got {:?}",
18756 self.peek()
18757 )));
18758 }
18759 self.advance();
18760 // `SET a = DEFAULT` rides the same `__column_default` marker
18761 // the VALUES-row and UPDATE-SET paths use; the INSERT
18762 // executor resolves it against the target column.
18763 if matches!(self.peek(), Token::Default) {
18764 self.advance();
18765 values.push(Expr::FunctionCall {
18766 name: "__column_default".to_string(),
18767 args: Vec::new(),
18768 });
18769 } else {
18770 values.push(self.parse_expr(0)?);
18771 }
18772 if matches!(self.peek(), Token::Comma) {
18773 self.advance();
18774 continue;
18775 }
18776 break;
18777 }
18778 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18779 let returning = self.parse_optional_returning()?;
18780 return Ok(Statement::Insert(InsertStatement {
18781 ctes: Vec::new(),
18782 table,
18783 alias,
18784 columns: Some(names),
18785 rows: alloc::vec![values],
18786 select_source: None,
18787 // MySQL's SET form has no `OVERRIDING …` clause (that is
18788 // PG's identity-column spelling).
18789 overriding: Overriding::None,
18790 mysql_ignore: ignore,
18791 on_conflict,
18792 returning,
18793 }));
18794 }
18795 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18796 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18797 // a parenthesized query source instead (PG select_with_parens:
18798 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18799 // both keywords are reserved in PG, so no column list can start
18800 // with them.
18801 let columns = if matches!(self.peek(), Token::LParen) {
18802 self.advance();
18803 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18804 let select_stmt = if self.peek_is_with_kw() {
18805 self.advance();
18806 self.parse_nested_with_select()?
18807 } else {
18808 match self.parse_select_stmt()? {
18809 Statement::Select(s) => s,
18810 other => {
18811 return Err(self.err(alloc::format!(
18812 "expected SELECT in parenthesized INSERT source, got {other:?}"
18813 )));
18814 }
18815 }
18816 };
18817 if !matches!(self.peek(), Token::RParen) {
18818 return Err(self.err(format!(
18819 "expected ')' after parenthesized INSERT source, got {:?}",
18820 self.peek()
18821 )));
18822 }
18823 self.advance();
18824 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18825 let returning = self.parse_optional_returning()?;
18826 return Ok(Statement::Insert(InsertStatement {
18827 ctes: Vec::new(),
18828 table,
18829 alias: alias.clone(),
18830 columns: None,
18831 rows: Vec::new(),
18832 select_source: Some(Box::new(select_stmt)),
18833 on_conflict,
18834 returning,
18835 overriding: Overriding::None,
18836 mysql_ignore: ignore,
18837 }));
18838 }
18839 let mut names = Vec::new();
18840 loop {
18841 names.push(self.expect_ident_like()?);
18842 match self.peek() {
18843 Token::Comma => {
18844 self.advance();
18845 }
18846 Token::RParen => {
18847 self.advance();
18848 break;
18849 }
18850 other => {
18851 return Err(self.err(format!(
18852 "expected ',' or ')' in INSERT column list, got {other:?}"
18853 )));
18854 }
18855 }
18856 }
18857 Some(names)
18858 } else {
18859 None
18860 };
18861 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18862 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18863 // is captured on the statement so the engine can apply PG's
18864 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18865 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18866 {
18867 self.advance();
18868 let which = self.expect_ident_like()?;
18869 let ov = if which.eq_ignore_ascii_case("system") {
18870 Overriding::System
18871 } else if which.eq_ignore_ascii_case("user") {
18872 Overriding::User
18873 } else {
18874 return Err(self.err(format!(
18875 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18876 )));
18877 };
18878 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18879 return Err(self.err(format!(
18880 "expected VALUE after OVERRIDING {}, got {:?}",
18881 which.to_ascii_uppercase(),
18882 self.peek()
18883 )));
18884 }
18885 self.advance();
18886 ov
18887 } else {
18888 Overriding::None
18889 };
18890 // `INSERT INTO t DEFAULT VALUES` — a single row made
18891 // entirely of column defaults. Lower to the permuted
18892 // column-list path with an empty list: every schema column
18893 // is unmapped, so the engine fills each from its default
18894 // (serials advance, plain defaults evaluate, the rest NULL).
18895 if matches!(self.peek(), Token::Default) {
18896 self.advance();
18897 if !matches!(self.peek(), Token::Values) {
18898 return Err(self.err(format!(
18899 "expected VALUES after DEFAULT in INSERT, got {:?}",
18900 self.peek()
18901 )));
18902 }
18903 self.advance();
18904 if columns.is_some() {
18905 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18906 }
18907 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18908 let returning = self.parse_optional_returning()?;
18909 return Ok(Statement::Insert(InsertStatement {
18910 ctes: Vec::new(),
18911 table,
18912 alias: alias.clone(),
18913 columns: Some(Vec::new()),
18914 rows: alloc::vec![Vec::new()],
18915 select_source: None,
18916 on_conflict,
18917 returning,
18918 overriding,
18919 mysql_ignore: ignore,
18920 }));
18921 }
18922 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18923 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18924 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18925 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18926 // own WITH comes before INSERT).
18927 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18928 let select_stmt = if self.peek_is_with_kw() {
18929 self.advance();
18930 self.parse_nested_with_select()?
18931 } else {
18932 match self.parse_select_stmt()? {
18933 Statement::Select(s) => s,
18934 other => {
18935 return Err(self.err(alloc::format!(
18936 "expected SELECT after INSERT INTO ... target, got {other:?}"
18937 )));
18938 }
18939 }
18940 };
18941 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18942 let returning = self.parse_optional_returning()?;
18943 return Ok(Statement::Insert(InsertStatement {
18944 ctes: Vec::new(),
18945 table,
18946 alias: alias.clone(),
18947 columns,
18948 rows: Vec::new(),
18949 select_source: Some(Box::new(select_stmt)),
18950 on_conflict,
18951 returning,
18952 overriding,
18953 mysql_ignore: ignore,
18954 }));
18955 }
18956 if !matches!(self.peek(), Token::Values) {
18957 return Err(self.err(format!(
18958 "expected VALUES or SELECT after table name, got {:?}",
18959 self.peek()
18960 )));
18961 }
18962 self.advance();
18963 if !matches!(self.peek(), Token::LParen) {
18964 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18965 }
18966 let mut rows = Vec::new();
18967 loop {
18968 // Each iteration consumes one `(expr, expr, …)` tuple.
18969 if !matches!(self.peek(), Token::LParen) {
18970 return Err(self.err(format!(
18971 "expected '(' for next VALUES tuple, got {:?}",
18972 self.peek()
18973 )));
18974 }
18975 self.advance();
18976 let mut tuple = Vec::new();
18977 loop {
18978 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18979 // the column's declared default for that slot. Rides out as the
18980 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18981 // path uses; the INSERT executor resolves it per target column.
18982 if matches!(self.peek(), Token::Default) {
18983 self.advance();
18984 tuple.push(Expr::FunctionCall {
18985 name: "__column_default".to_string(),
18986 args: Vec::new(),
18987 });
18988 } else {
18989 tuple.push(self.parse_expr(0)?);
18990 }
18991 match self.peek() {
18992 Token::Comma => {
18993 self.advance();
18994 }
18995 Token::RParen => {
18996 self.advance();
18997 break;
18998 }
18999 other => {
19000 return Err(self.err(format!(
19001 "expected ',' or ')' in VALUES tuple, got {other:?}"
19002 )));
19003 }
19004 }
19005 }
19006 if tuple.is_empty() {
19007 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
19008 }
19009 rows.push(tuple);
19010 // Continue with comma-separated tuples.
19011 if matches!(self.peek(), Token::Comma) {
19012 self.advance();
19013 } else {
19014 break;
19015 }
19016 }
19017 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
19018 // to ON CONFLICT DO UPDATE with an empty conflict target
19019 // (the engine picks the table's first unique index, which
19020 // matches MySQL's any-unique-key behaviour for the common
19021 // single-key case). `VALUES(col)` in the assignments is
19022 // MySQL's spelling of EXCLUDED.col.
19023 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
19024 let returning = self.parse_optional_returning()?;
19025 Ok(Statement::Insert(InsertStatement {
19026 ctes: Vec::new(),
19027 table,
19028 alias,
19029 columns,
19030 rows,
19031 select_source: None,
19032 on_conflict,
19033 returning,
19034 overriding,
19035 mysql_ignore: ignore,
19036 }))
19037 }
19038
19039 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
19040 /// the incoming row's value — exactly PG's EXCLUDED.col.
19041 fn rewrite_mysql_values_refs(e: &mut Expr) {
19042 match e {
19043 Expr::FunctionCall { name, args }
19044 if name.eq_ignore_ascii_case("values")
19045 && args.len() == 1
19046 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19047 {
19048 let Expr::Column(c) = &args[0] else {
19049 unreachable!("guarded above");
19050 };
19051 *e = Expr::Column(crate::ast::ColumnName {
19052 qualifier: Some("EXCLUDED".to_string()),
19053 name: c.name.clone(),
19054 });
19055 }
19056 Expr::FunctionCall { args, .. } => {
19057 for a in args {
19058 Self::rewrite_mysql_values_refs(a);
19059 }
19060 }
19061 Expr::Binary { lhs, rhs, .. } => {
19062 Self::rewrite_mysql_values_refs(lhs);
19063 Self::rewrite_mysql_values_refs(rhs);
19064 }
19065 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19066 Self::rewrite_mysql_values_refs(expr);
19067 }
19068 Expr::Case {
19069 operand,
19070 branches,
19071 else_branch,
19072 } => {
19073 if let Some(op) = operand {
19074 Self::rewrite_mysql_values_refs(op);
19075 }
19076 for (w, t) in branches {
19077 Self::rewrite_mysql_values_refs(w);
19078 Self::rewrite_mysql_values_refs(t);
19079 }
19080 if let Some(el) = else_branch {
19081 Self::rewrite_mysql_values_refs(el);
19082 }
19083 }
19084 _ => {}
19085 }
19086 }
19087
19088 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19089 /// clause sitting between the INSERT body and the trailing
19090 /// RETURNING. All keywords come in as bare idents; `ON` is
19091 /// a reserved Token though.
19092 fn parse_optional_on_conflict(
19093 &mut self,
19094 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19095 if !matches!(self.peek(), Token::On) {
19096 return Ok(None);
19097 }
19098 // Peek further: we want exactly "ON CONFLICT ...". If the
19099 // next ident isn't "conflict", let some other parser handle.
19100 let next_is_conflict = matches!(
19101 self.tokens.get(self.pos + 1),
19102 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19103 );
19104 if !next_is_conflict {
19105 return Ok(None);
19106 }
19107 self.advance(); // ON
19108 self.advance(); // CONFLICT
19109 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19110 // the constraint instead of listing columns (the pg_dump
19111 // form); the engine resolves it.
19112 let mut constraint_name: Option<String> = None;
19113 if matches!(self.peek(), Token::On) {
19114 self.advance(); // ON
19115 match self.advance() {
19116 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19117 }
19118 other => {
19119 return Err(self.err(alloc::format!(
19120 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19121 )));
19122 }
19123 }
19124 constraint_name = Some(self.expect_ident_like()?);
19125 }
19126 // Optional `(col [, col]*)` target list.
19127 let mut target_columns: Vec<String> = Vec::new();
19128 if matches!(self.peek(), Token::LParen) {
19129 self.advance();
19130 loop {
19131 target_columns.push(self.expect_ident_like()?);
19132 match self.peek() {
19133 Token::Comma => {
19134 self.advance();
19135 }
19136 Token::RParen => {
19137 self.advance();
19138 break;
19139 }
19140 other => {
19141 return Err(self.err(alloc::format!(
19142 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19143 )));
19144 }
19145 }
19146 }
19147 }
19148 // v7.39 (round 240) — optional index predicate after the target
19149 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19150 // PARTIAL unique index; SPG's arbiters are full indexes, which
19151 // satisfy any predicate, so it is parsed and carried but not
19152 // consulted (recorded residual: partial-unique-index arbiters).
19153 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19154 self.advance();
19155 Some(self.parse_expr(0)?)
19156 } else {
19157 None
19158 };
19159 // Required `DO`.
19160 match self.advance() {
19161 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19162 other => {
19163 return Err(self.err(alloc::format!(
19164 "expected DO after ON CONFLICT [(…)], got {other:?}"
19165 )));
19166 }
19167 }
19168 // Action: NOTHING | UPDATE SET …
19169 let action = match self.advance() {
19170 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19171 crate::ast::OnConflictAction::Nothing
19172 }
19173 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19174 self.parse_on_conflict_update_action()?
19175 }
19176 other => {
19177 return Err(self.err(alloc::format!(
19178 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19179 )));
19180 }
19181 };
19182 Ok(Some(crate::ast::OnConflictClause {
19183 target_columns,
19184 index_where,
19185 constraint_name,
19186 mysql_lowered: false,
19187 action,
19188 }))
19189 }
19190
19191 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19192 /// `SET col = expr [, …] [WHERE cond]`. Caller already
19193 /// consumed `UPDATE`.
19194 fn parse_on_conflict_update_action(
19195 &mut self,
19196 ) -> Result<crate::ast::OnConflictAction, ParseError> {
19197 // `SET`
19198 match self.advance() {
19199 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19200 other => {
19201 return Err(self.err(alloc::format!(
19202 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19203 )));
19204 }
19205 }
19206 let mut assignments: Vec<(String, Expr)> = Vec::new();
19207 loop {
19208 let col = self.expect_ident_like()?;
19209 if !matches!(self.peek(), Token::Eq) {
19210 return Err(self.err(alloc::format!(
19211 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19212 self.peek()
19213 )));
19214 }
19215 self.advance();
19216 let value = self.parse_expr(0)?;
19217 assignments.push((col, value));
19218 if matches!(self.peek(), Token::Comma) {
19219 self.advance();
19220 continue;
19221 }
19222 break;
19223 }
19224 let where_ = if matches!(self.peek(), Token::Where) {
19225 self.advance();
19226 Some(self.parse_expr(0)?)
19227 } else {
19228 None
19229 };
19230 Ok(crate::ast::OnConflictAction::Update {
19231 assignments,
19232 where_,
19233 })
19234 }
19235
19236 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19237 let mut items = Vec::new();
19238 // v7.39 (round 341, V66) — PG's target list may be EMPTY
19239 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19240 // answers one zero-column row per row of t, and a bare `SELECT`
19241 // answers a single zero-column row. SPG required at least one
19242 // item, so both were syntax errors. Recognised by the token that
19243 // follows — nothing that can start an expression appears here.
19244 if self.select_list_is_empty_here() {
19245 return Ok(items);
19246 }
19247 loop {
19248 items.push(self.parse_select_item()?);
19249 if matches!(self.peek(), Token::Comma) {
19250 self.advance();
19251 } else {
19252 break;
19253 }
19254 }
19255 Ok(items)
19256 }
19257
19258 /// Is the target list empty at this point — i.e. does the next token
19259 /// end the SELECT's item list rather than start an item?
19260 fn select_list_is_empty_here(&self) -> bool {
19261 match self.peek() {
19262 Token::From
19263 | Token::Where
19264 | Token::Group
19265 | Token::Having
19266 | Token::Order
19267 | Token::Limit
19268 | Token::Offset
19269 | Token::Semicolon
19270 | Token::RParen
19271 | Token::Union
19272 | Token::Except
19273 | Token::Eof => true,
19274 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19275 // with unreserved keywords, so they arrive as plain idents.
19276 Token::Ident(s) => {
19277 s.eq_ignore_ascii_case("fetch")
19278 || s.eq_ignore_ascii_case("window")
19279 || s.eq_ignore_ascii_case("intersect")
19280 }
19281 _ => false,
19282 }
19283 }
19284
19285 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19286 if matches!(self.peek(), Token::Star) {
19287 self.advance();
19288 return Ok(SelectItem::Wildcard);
19289 }
19290 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19291 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19292 // choke on the `*` ("expected identifier, got Star"). The lookahead is
19293 // `<ident> . *` with nothing binding tighter.
19294 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19295 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19296 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19297 {
19298 self.advance(); // qualifier
19299 self.advance(); // .
19300 self.advance(); // *
19301 return Ok(SelectItem::QualifiedWildcard(q));
19302 }
19303 }
19304 let start_tok = self.pos;
19305 let expr = self.parse_expr(0)?;
19306 let end_tok = self.consumed_pos();
19307 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19308 // multi-column function returns into columns. Marked here and lowered in
19309 // `parse_bare_select`, where the FROM clause is in hand.
19310 if matches!(self.peek(), Token::Dot)
19311 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19312 {
19313 self.advance(); // .
19314 self.advance(); // *
19315 return Ok(SelectItem::Expr {
19316 expr: Expr::FunctionCall {
19317 name: "__record_expand".to_string(),
19318 args: alloc::vec![expr],
19319 },
19320 alias: None,
19321 });
19322 }
19323 // v7.39.2 — MySQL lets a STRING name a projection item, with or
19324 // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19325 // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19326 // `syntax error at or near "'x'"` to all of them.
19327 //
19328 // Only here, not in `parse_optional_alias`: that one also names
19329 // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19330 // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19331 // after the lexer's own rule has joined adjacent literals, or
19332 // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19333 // MySQL answers the concatenation `ab`.
19334 if self.mysql_dialect {
19335 let at_as = matches!(self.peek(), Token::As)
19336 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19337 if at_as {
19338 self.advance();
19339 }
19340 if let Token::String(name) = self.peek().clone() {
19341 self.advance();
19342 return Ok(SelectItem::Expr {
19343 expr,
19344 alias: Some(name),
19345 });
19346 }
19347 }
19348 let alias = match self.parse_optional_alias()? {
19349 Some(a) => Some(a),
19350 None => self.mysql_item_label(&expr, start_tok, end_tok),
19351 };
19352 Ok(SelectItem::Expr { expr, alias })
19353 }
19354
19355 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19356 /// carries no `AS`, filled in here so every downstream path reports it
19357 /// without knowing the rule. `None` leaves the item un-aliased, which is
19358 /// what a PG session always gets.
19359 ///
19360 /// Measured against MariaDB 11, three rules and no more:
19361 ///
19362 /// | item | label | why |
19363 /// |------------------|------------|------------------------------|
19364 /// | `lbl.a` | `a` | a column reports its name |
19365 /// | `'it''s'` | `it's` | a string reports its VALUE |
19366 /// | `a + b` | `a + b` | anything else, source text |
19367 ///
19368 /// The third is why this lives in the parser at all: the label is the
19369 /// text the client WROTE, down to the spacing, so it cannot be printed
19370 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19371 ///
19372 /// Comments survive, and that is right: through a `mariadb` CLI both
19373 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
19374 /// CLIENT stripping the comment before it sends. Asked over the raw
19375 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19376 /// produces.
19377 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19378 if !self.mysql_dialect {
19379 return None;
19380 }
19381 match expr {
19382 // A column already reports its own name downstream; naming it
19383 // again here would only re-state the qualifier the label drops.
19384 Expr::Column(_) => None,
19385 // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19386 // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19387 // the first segment as written, not the joined value
19388 // (measured). The lexer logs where it joined them.
19389 Expr::Literal(Literal::String(v)) => Some(
19390 self.merged_first_len(start_tok)
19391 .and_then(|n| v.get(..n))
19392 .map_or_else(|| v.clone(), String::from),
19393 ),
19394 _ => self.source_span(start_tok, end_tok).map(str::to_string),
19395 }
19396 }
19397
19398 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19399 /// consumed VALUES keyword. Each row lowers to a constant SELECT
19400 /// with PG's default column1..columnN names; subsequent rows
19401 /// chain as UNION ALL peers. Shared by the FROM-position
19402 /// `( VALUES … )` arm and the top-level bare VALUES statement.
19403 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19404 let mut row_selects: Vec<SelectStatement> = Vec::new();
19405 loop {
19406 if !matches!(self.peek(), Token::LParen) {
19407 return Err(self.err(alloc::format!(
19408 "expected '(' to start a VALUES row, got {:?}",
19409 self.peek()
19410 )));
19411 }
19412 self.advance(); // (
19413 let mut items: Vec<SelectItem> = Vec::new();
19414 loop {
19415 let expr = self.parse_expr(0)?;
19416 items.push(SelectItem::Expr {
19417 expr,
19418 alias: Some(alloc::format!("column{}", items.len() + 1)),
19419 });
19420 match self.peek() {
19421 Token::Comma => {
19422 self.advance();
19423 }
19424 Token::RParen => break,
19425 other => {
19426 return Err(self.err(alloc::format!(
19427 "expected ',' or ')' in VALUES row, got {other:?}"
19428 )));
19429 }
19430 }
19431 }
19432 self.advance(); // )
19433 row_selects.push(SelectStatement {
19434 locking: None,
19435 ctes: Vec::new(),
19436 distinct: false,
19437 distinct_on: Vec::new(),
19438 items,
19439 from: None,
19440 where_: None,
19441 group_by: None,
19442 group_by_all: false,
19443 having: None,
19444 unions: Vec::new(),
19445 order_by: Vec::new(),
19446 limit: None,
19447 offset: None,
19448 limit_with_ties: false,
19449 window_check_exprs: Vec::new(),
19450 });
19451 if matches!(self.peek(), Token::Comma) {
19452 self.advance();
19453 continue;
19454 }
19455 break;
19456 }
19457 let mut head = row_selects.remove(0);
19458 head.unions = row_selects
19459 .into_iter()
19460 .map(|s| (UnionKind::All, s))
19461 .collect();
19462 Ok(head)
19463 }
19464
19465 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19466 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19467 // children. It was read as a table NAMED `only`, so the query
19468 // failed on `relation "only" does not exist`.
19469 //
19470 // v7.39 (round 644) — and it is no longer a no-op. Round 621
19471 // absorbed the keyword, reasoning that SPG's children are
19472 // separate relations a plain scan does not descend into, so ONLY
19473 // already described the scan. That stopped being true when a
19474 // partition parent started unioning its children: measured,
19475 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19476 // where PG answers 0. The flag is carried now.
19477 let mut only = false;
19478 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19479 && matches!(
19480 self.tokens.get(self.pos + 1),
19481 Some(Token::Ident(_) | Token::QuotedIdent(_))
19482 )
19483 {
19484 only = true;
19485 self.advance();
19486 }
19487 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19488 // for these SRFs the keyword is noise at parse time: the
19489 // join executor already substitutes outer-column references
19490 // into unnest_expr / generate_series_args per outer row
19491 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19492 // licences the correlation even without the keyword. Absorb
19493 // it and fall through to the SRF arms below.
19494 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19495 // just the four builtin SRFs: a user set-returning function on a JOIN's
19496 // right side is the whole point of LATERAL. The keyword stays noise at
19497 // parse time — the join executor substitutes the outer row into the
19498 // call's arguments per outer row.
19499 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19500 && matches!(
19501 self.tokens.get(self.pos + 1),
19502 // The json_each family has its OWN `LATERAL …` arm below, which
19503 // needs to see the keyword — absorbing it here would send those
19504 // calls down the generic table-function channel instead.
19505 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19506 )
19507 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19508 {
19509 self.advance(); // LATERAL
19510 }
19511 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19512 // set-returning function whose argument may reference a
19513 // preceding FROM item. We rewrite this to
19514 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19515 // AS __srf__) AS <alias>` so the existing LATERAL subquery
19516 // executor handles per-outer-row evaluation and the
19517 // SRF-primary jsonb_each_text path handles the inner
19518 // materialisation. Sentori 0067 backfill is the dogfood
19519 // shape.
19520 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19521 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19522 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19523 {
19524 self.advance(); // LATERAL
19525 let each_fn = match self.peek() {
19526 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19527 _ => unreachable!(),
19528 };
19529 self.advance(); // jsonb_each[_text] / json_each[_text]
19530 self.advance(); // (
19531 let arg = self.parse_expr(0)?;
19532 if !matches!(self.peek(), Token::RParen) {
19533 return Err(self.err(alloc::format!(
19534 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19535 self.peek()
19536 )));
19537 }
19538 self.advance();
19539 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19540 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19541 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19542 // FROM jsonb_each_text(<arg>) AS __srf__
19543 // PG's `AS kv(key, value)` column-alias list maps
19544 // positions to names; default to (key, value) when
19545 // omitted (matching the SRF's natural column names).
19546 let srf_alias = "__srf__".to_string();
19547 let key_alias = column_aliases
19548 .first()
19549 .cloned()
19550 .unwrap_or_else(|| "key".to_string());
19551 let value_alias = column_aliases
19552 .get(1)
19553 .cloned()
19554 .unwrap_or_else(|| "value".to_string());
19555 let inner_select = crate::ast::SelectStatement {
19556 locking: None,
19557 ctes: Vec::new(),
19558 distinct: false,
19559 distinct_on: Vec::new(),
19560 items: alloc::vec![
19561 crate::ast::SelectItem::Expr {
19562 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19563 qualifier: Some(srf_alias.clone()),
19564 name: "key".to_string(),
19565 }),
19566 alias: Some(key_alias),
19567 },
19568 crate::ast::SelectItem::Expr {
19569 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19570 qualifier: Some(srf_alias.clone()),
19571 name: "value".to_string(),
19572 }),
19573 alias: Some(value_alias),
19574 },
19575 ],
19576 from: Some(crate::ast::FromClause {
19577 primary: TableRef {
19578 name: srf_alias.clone(),
19579 alias: Some(srf_alias.clone()),
19580 only: false,
19581 as_of_segment: None,
19582 unnest_expr: None,
19583 unnest_column_aliases: Vec::new(),
19584 with_ordinality: false,
19585 generate_series_args: None,
19586 lateral_subquery: None,
19587 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19588 table_fn_call: None,
19589 rows_from: None,
19590 json_table: None,
19591 scalar_fn_item: false,
19592 },
19593 joins: Vec::new(),
19594 }),
19595 where_: None,
19596 group_by: None,
19597 group_by_all: false,
19598 having: None,
19599 unions: Vec::new(),
19600 order_by: Vec::new(),
19601 limit: None,
19602 offset: None,
19603 limit_with_ties: false,
19604 window_check_exprs: Vec::new(),
19605 };
19606 return Ok(TableRef {
19607 name: alias.clone(),
19608 alias: Some(alias),
19609 only: false,
19610 as_of_segment: None,
19611 unnest_expr: None,
19612 unnest_column_aliases: Vec::new(),
19613 with_ordinality: false,
19614 generate_series_args: None,
19615 lateral_subquery: Some(Box::new(inner_select)),
19616 jsonb_each_text_arg: None,
19617 table_fn_call: None,
19618 rows_from: None,
19619 json_table: None,
19620 scalar_fn_item: false,
19621 });
19622 }
19623 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19624 // without an explicit `LATERAL` keyword is the same shape
19625 // PG accepts (SRF naturally licences lateral correlation).
19626 // We mirror the LATERAL rewrite when the argument syntactic-
19627 // ally references an outer column (Column { qualifier:
19628 // Some(_), … }). For simplicity we apply the rewrite
19629 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19630 // in the FROM-list — caller-side join parsing positions
19631 // this peek correctly.
19632 // (Implementation note: detection lives below; the LATERAL
19633 // branch above already covers the explicit form; the bare
19634 // form falls through to the plain SRF arm and the engine
19635 // treats it as a constant-arg SRF if no outer reference is
19636 // present.)
19637 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19638 // table. Detect at the head so it claims precedence over
19639 // every other table-ref shape (unnest / generate_series /
19640 // bare ident); the lateral subquery itself follows the
19641 // regular SELECT grammar.
19642 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19643 // t(cols)`. Each row lowers to a constant SELECT with PG's
19644 // default column1..columnN names; subsequent rows chain as
19645 // UNION ALL peers. The result rides the derived-table
19646 // lateral_subquery channel — zero executor work.
19647 if matches!(self.peek(), Token::LParen)
19648 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19649 {
19650 self.advance(); // (
19651 self.advance(); // VALUES
19652 let head = self.parse_values_rows_body()?;
19653 if !matches!(self.peek(), Token::RParen) {
19654 return Err(self.err(alloc::format!(
19655 "expected ')' after VALUES list, got {:?}",
19656 self.peek()
19657 )));
19658 }
19659 self.advance();
19660 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19661 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19662 return Ok(TableRef {
19663 name,
19664 alias: alias_ident,
19665 only: false,
19666 as_of_segment: None,
19667 unnest_expr: None,
19668 unnest_column_aliases: column_aliases,
19669 with_ordinality: false,
19670 generate_series_args: None,
19671 lateral_subquery: Some(Box::new(head)),
19672 jsonb_each_text_arg: None,
19673 table_fn_call: None,
19674 rows_from: None,
19675 json_table: None,
19676 scalar_fn_item: false,
19677 });
19678 }
19679 // v7.37.17 (17.6 siblings) — plain derived table:
19680 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19681 // lateral_subquery channel the explicit LATERAL form uses —
19682 // an uncorrelated inner SELECT executes identically. The
19683 // inner parse carries UNION tails (they live on
19684 // SelectStatement.unions).
19685 // v7.37 D.20 — the derived-table inner may itself be a
19686 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19687 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19688 // bare `(SELECT …)`. parse_one_statement already routes a leading
19689 // `(` set-op group (its LParen arm) and a leading WITH
19690 // (parse_with_cte_then_select), so widen the second-token gate to
19691 // Select | LParen | WITH.
19692 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19693 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19694 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19695 // has existed since the shorthand landed and `parse_bare_select`
19696 // already routes it ("valid anywhere a SELECT head is"); what was
19697 // missing is this second-token gate, and the CTE body's dispatch
19698 // below. Round 868 found both by putting the shorthand in a
19699 // subquery — the top-level forms had been the only ones tested.
19700 if matches!(self.peek(), Token::LParen)
19701 && (matches!(
19702 self.tokens.get(self.pos + 1),
19703 Some(Token::Select | Token::LParen | Token::Table)
19704 ) || matches!(self.tokens.get(self.pos + 1),
19705 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19706 {
19707 self.advance(); // (
19708 let inner = match self.parse_one_statement()? {
19709 Statement::Select(s) => s,
19710 other => {
19711 return Err(self.err(alloc::format!(
19712 "expected SELECT inside derived table ( … ), got {other:?}"
19713 )));
19714 }
19715 };
19716 if !matches!(self.peek(), Token::RParen) {
19717 return Err(self.err(alloc::format!(
19718 "expected ')' after derived-table subquery, got {:?}",
19719 self.peek()
19720 )));
19721 }
19722 self.advance();
19723 // `AS t(a, b)` column-alias list rides the
19724 // unnest_column_aliases field (same positional-rename
19725 // contract the unnest SRFs use).
19726 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19727 let name = alias_ident
19728 .clone()
19729 .unwrap_or_else(|| "subquery".to_string());
19730 return Ok(TableRef {
19731 name,
19732 alias: alias_ident,
19733 only: false,
19734 as_of_segment: None,
19735 unnest_expr: None,
19736 unnest_column_aliases: column_aliases,
19737 with_ordinality: false,
19738 generate_series_args: None,
19739 lateral_subquery: Some(Box::new(inner)),
19740 jsonb_each_text_arg: None,
19741 table_fn_call: None,
19742 rows_from: None,
19743 json_table: None,
19744 scalar_fn_item: false,
19745 });
19746 }
19747 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19748 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19749 {
19750 self.advance(); // LATERAL
19751 self.advance(); // (
19752 // Parse the inner SELECT.
19753 let inner = match self.parse_one_statement()? {
19754 Statement::Select(s) => s,
19755 other => {
19756 return Err(self.err(alloc::format!(
19757 "expected SELECT inside LATERAL ( … ), got {other:?}"
19758 )));
19759 }
19760 };
19761 if !matches!(self.peek(), Token::RParen) {
19762 return Err(self.err(alloc::format!(
19763 "expected ')' after LATERAL subquery, got {:?}",
19764 self.peek()
19765 )));
19766 }
19767 self.advance();
19768 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19769 // `(VALUES …) t(g)` derived table round-trips through view-body
19770 // Display, which renders on the lateral_subquery channel).
19771 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19772 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19773 return Ok(TableRef {
19774 name,
19775 alias: alias_ident,
19776 only: false,
19777 as_of_segment: None,
19778 unnest_expr: None,
19779 unnest_column_aliases: column_aliases,
19780 with_ordinality: false,
19781 generate_series_args: None,
19782 lateral_subquery: Some(Box::new(inner)),
19783 jsonb_each_text_arg: None,
19784 table_fn_call: None,
19785 rows_from: None,
19786 json_table: None,
19787 scalar_fn_item: false,
19788 });
19789 }
19790 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19791 // function as a FROM item. Emits one row per (key, value)
19792 // pair in the JSONB object argument as TEXT columns. May
19793 // be wrapped in CROSS JOIN LATERAL when the argument
19794 // references a preceding FROM item (sentori migration
19795 // 0067 backfill shape: `CROSS JOIN LATERAL
19796 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19797 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19798 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19799 {
19800 let each_fn = match self.peek() {
19801 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19802 _ => unreachable!(),
19803 };
19804 self.advance(); // jsonb_each[_text] / json_each[_text]
19805 self.advance(); // (
19806 let arg = self.parse_expr(0)?;
19807 if !matches!(self.peek(), Token::RParen) {
19808 return Err(self.err(alloc::format!(
19809 "expected ')' after {each_fn}() argument, got {:?}",
19810 self.peek()
19811 )));
19812 }
19813 self.advance();
19814 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19815 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19816 return Ok(TableRef {
19817 name,
19818 alias: alias_ident,
19819 only: false,
19820 as_of_segment: None,
19821 unnest_expr: None,
19822 // `AS t(k, v)` renames key/value positionally, same as the
19823 // LATERAL-position form already does.
19824 unnest_column_aliases: column_aliases,
19825 with_ordinality: false,
19826 generate_series_args: None,
19827 lateral_subquery: None,
19828 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19829 table_fn_call: None,
19830 rows_from: None,
19831 json_table: None,
19832 scalar_fn_item: false,
19833 });
19834 }
19835 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19836 // (+ json_ variants) — record-returning JSON functions with a
19837 // column-definition list. Desugar to a derived table that
19838 // projects each declared column from the JSON via `->>` + a cast,
19839 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19840 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19841 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19842 {
19843 return self.parse_json_to_record_from();
19844 }
19845 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19846 // row is a text[] of capture groups, so it cannot desugar to unnest
19847 // (that would flatten the array). Wrap it as a derived table
19848 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19849 // SRF path already emits one text[] row per match. PG names the column
19850 // `regexp_matches`; an `AS a(col)` alias overrides it.
19851 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19852 if s.eq_ignore_ascii_case("regexp_matches"))
19853 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19854 {
19855 self.advance(); // fn name
19856 self.advance(); // (
19857 let mut fn_args: Vec<Expr> = Vec::new();
19858 loop {
19859 fn_args.push(self.parse_expr(0)?);
19860 if matches!(self.peek(), Token::Comma) {
19861 self.advance();
19862 continue;
19863 }
19864 break;
19865 }
19866 if !matches!(self.peek(), Token::RParen) {
19867 return Err(self.err(alloc::format!(
19868 "expected ')' after regexp_matches() arguments, got {:?}",
19869 self.peek()
19870 )));
19871 }
19872 self.advance();
19873 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19874 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19875 // it, so it died on the `with` token while every other table function
19876 // accepted it.
19877 let with_ordinality = self.absorb_with_ordinality();
19878 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19879 let table_alias = alias_ident
19880 .clone()
19881 .unwrap_or_else(|| "regexp_matches".to_string());
19882 // PG names a single-column function's output column after the ALIAS
19883 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19884 // `m` reads as that column and not as a whole-row composite. Naming
19885 // it after the function regardless made `SELECT m[1] FROM … AS m`
19886 // subscript a record.
19887 let col_name = column_aliases
19888 .first()
19889 .cloned()
19890 .or_else(|| alias_ident.clone())
19891 .unwrap_or_else(|| "regexp_matches".to_string());
19892 let inner = crate::ast::SelectStatement {
19893 locking: None,
19894 ctes: Vec::new(),
19895 distinct: false,
19896 distinct_on: Vec::new(),
19897 items: alloc::vec![SelectItem::Expr {
19898 expr: Expr::FunctionCall {
19899 name: "regexp_matches".to_string(),
19900 args: fn_args,
19901 },
19902 alias: Some(col_name),
19903 }],
19904 from: None,
19905 where_: None,
19906 group_by: None,
19907 group_by_all: false,
19908 having: None,
19909 unions: Vec::new(),
19910 order_by: Vec::new(),
19911 limit: None,
19912 offset: None,
19913 limit_with_ties: false,
19914 window_check_exprs: Vec::new(),
19915 };
19916 return Ok(TableRef {
19917 name: table_alias.clone(),
19918 alias: Some(table_alias),
19919 only: false,
19920 as_of_segment: None,
19921 unnest_expr: None,
19922 unnest_column_aliases: column_aliases,
19923 with_ordinality,
19924 generate_series_args: None,
19925 lateral_subquery: Some(Box::new(inner)),
19926 jsonb_each_text_arg: None,
19927 table_fn_call: None,
19928 rows_from: None,
19929 json_table: None,
19930 // regexp_matches returns text[], a base type: `SELECT m FROM
19931 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19932 scalar_fn_item: true,
19933 });
19934 }
19935 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19936 // / json_ variants as a FROM item. Rewritten into
19937 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19938 // elements as a TEXT array, and the existing unnest SRF path
19939 // materialises one row per element. PG's natural column name
19940 // is `value`; an `AS a(col)` column-alias list overrides it.
19941 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19942 if s.eq_ignore_ascii_case("jsonb_array_elements")
19943 || s.eq_ignore_ascii_case("json_array_elements")
19944 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19945 || s.eq_ignore_ascii_case("json_array_elements_text")
19946 || s.eq_ignore_ascii_case("jsonb_object_keys")
19947 || s.eq_ignore_ascii_case("json_object_keys")
19948 || s.eq_ignore_ascii_case("jsonb_path_query")
19949 || s.eq_ignore_ascii_case("json_path_query")
19950 || s.eq_ignore_ascii_case("generate_subscripts")
19951 || s.eq_ignore_ascii_case("string_to_table")
19952 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19953 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19954 {
19955 let fn_name = match self.peek() {
19956 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19957 _ => unreachable!(),
19958 };
19959 self.advance(); // fn name
19960 self.advance(); // (
19961 let mut fn_args: Vec<Expr> = Vec::new();
19962 loop {
19963 fn_args.push(self.parse_expr(0)?);
19964 if matches!(self.peek(), Token::Comma) {
19965 self.advance();
19966 continue;
19967 }
19968 break;
19969 }
19970 if !matches!(self.peek(), Token::RParen) {
19971 return Err(self.err(alloc::format!(
19972 "expected ')' after {fn_name}() arguments, got {:?}",
19973 self.peek()
19974 )));
19975 }
19976 self.advance();
19977 let with_ordinality = self.absorb_with_ordinality();
19978 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19979 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19980 // PG's natural column name: the array-elements SRFs
19981 // declare an OUT parameter `value`; jsonb_object_keys
19982 // and generate_subscripts have none, so the column is
19983 // named after the function. A bare table alias on a
19984 // single-column SRF renames the column too (PG: `FROM
19985 // generate_subscripts(a, 1) AS s` projects column s) —
19986 // except for the OUT-parameter SRFs, whose column stays
19987 // `value` under a bare alias.
19988 let natural_col = if fn_name.ends_with("_array_elements")
19989 || fn_name.ends_with("_array_elements_text")
19990 {
19991 "value".to_string()
19992 } else {
19993 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19994 };
19995 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19996 // Keep any further entries — the second names the
19997 // ordinality column under WITH ORDINALITY.
19998 srf_cols.extend(column_aliases.into_iter().skip(1));
19999 // The *_to_table SRFs are row-streams over the existing
20000 // *_to_array scalars — map the call target; the display
20001 // name (alias / column defaults) keeps the SRF spelling.
20002 let call_name = match fn_name.as_str() {
20003 "string_to_table" => "string_to_array".to_string(),
20004 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
20005 _ => fn_name,
20006 };
20007 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
20008 // preceding FROM item (bare or qualified column) is correlated;
20009 // route it through the per-outer-row lateral channel.
20010 let expr = crate::ast::Expr::FunctionCall {
20011 name: call_name,
20012 args: fn_args,
20013 };
20014 let correlated = Self::expr_has_any_column(&expr);
20015 let tref = TableRef {
20016 name,
20017 alias: alias_ident,
20018 only: false,
20019 as_of_segment: None,
20020 unnest_expr: Some(Box::new(expr)),
20021 unnest_column_aliases: srf_cols,
20022 with_ordinality,
20023 generate_series_args: None,
20024 lateral_subquery: None,
20025 jsonb_each_text_arg: None,
20026 table_fn_call: None,
20027 rows_from: None,
20028 json_table: None,
20029 // Each of these returns a BASE type (jsonb / text / int), so the item's
20030 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
20031 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
20032 scalar_fn_item: !with_ordinality,
20033 };
20034 return Ok(if correlated {
20035 Self::wrap_correlated_srf(tref)
20036 } else {
20037 tref
20038 });
20039 }
20040 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
20041 // explicit parallel-zip syntax. Each entry lowers to its
20042 // array-returning scalar form (unnest(x) → x itself; the
20043 // FROM-SRF rewrite family → their scalar array calls) and
20044 // the list rides the multi-arg unnest zip channel:
20045 // NULL-padded to the longest, WITH ORDINALITY appends the
20046 // counter. generate_series has no scalar array form and
20047 // errors honestly.
20048 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20049 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20050 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20051 {
20052 self.advance(); // ROWS
20053 self.advance(); // FROM
20054 self.advance(); // (
20055 let mut entries: Vec<Expr> = Vec::new();
20056 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20057 // Used only when some entry has no array form.
20058 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20059 loop {
20060 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20061 if !matches!(self.peek(), Token::LParen) {
20062 return Err(self.err(alloc::format!(
20063 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20064 self.peek()
20065 )));
20066 }
20067 self.advance();
20068 let mut fn_args: Vec<Expr> = Vec::new();
20069 if !matches!(self.peek(), Token::RParen) {
20070 loop {
20071 fn_args.push(self.parse_expr(0)?);
20072 if matches!(self.peek(), Token::Comma) {
20073 self.advance();
20074 continue;
20075 }
20076 break;
20077 }
20078 }
20079 if !matches!(self.peek(), Token::RParen) {
20080 return Err(self.err(alloc::format!(
20081 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20082 self.peek()
20083 )));
20084 }
20085 self.advance();
20086 let entry = match fn_name.as_str() {
20087 "unnest" => {
20088 if fn_args.len() != 1 {
20089 return Err(
20090 self.err("unnest inside ROWS FROM takes exactly one array".into())
20091 );
20092 }
20093 fn_args.pop().expect("len checked")
20094 }
20095 "jsonb_array_elements"
20096 | "json_array_elements"
20097 | "jsonb_array_elements_text"
20098 | "json_array_elements_text"
20099 | "jsonb_object_keys"
20100 | "json_object_keys"
20101 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20102 name: fn_name,
20103 args: fn_args,
20104 },
20105 "string_to_table" => crate::ast::Expr::FunctionCall {
20106 name: "string_to_array".to_string(),
20107 args: fn_args,
20108 },
20109 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20110 name: "regexp_split_to_array".to_string(),
20111 args: fn_args,
20112 },
20113 // v7.39 (read01 round 74) — an SRF with no array form
20114 // (`generate_series`, a user `RETURNS SETOF` function) has no
20115 // scalar expression to zip, so the WHOLE list switches to the
20116 // rows_from channel, which runs each function and zips the
20117 // rows themselves. The all-array case keeps the old lowering:
20118 // it is well-trodden and this must not disturb it.
20119 _ => {
20120 generic.push((fn_name, fn_args));
20121 if matches!(self.peek(), Token::Comma) {
20122 self.advance();
20123 continue;
20124 }
20125 break;
20126 }
20127 };
20128 generic.push((
20129 // The array-able entries carry their lowered expr along, so a
20130 // MIXED list still works: the engine sees the scalar array
20131 // form and unnests it.
20132 "__array".to_string(),
20133 alloc::vec![entry.clone()],
20134 ));
20135 entries.push(entry);
20136 if matches!(self.peek(), Token::Comma) {
20137 self.advance();
20138 continue;
20139 }
20140 break;
20141 }
20142 if !matches!(self.peek(), Token::RParen) {
20143 return Err(self.err(alloc::format!(
20144 "expected ')' to close ROWS FROM, got {:?}",
20145 self.peek()
20146 )));
20147 }
20148 self.advance();
20149 let with_ordinality = self.absorb_with_ordinality();
20150 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20151 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20152 // v7.39 (read01 round 74) — some entry had no array form, so the whole
20153 // list rides the generic channel.
20154 if generic.iter().any(|(n, _)| n != "__array") {
20155 let correlated = generic
20156 .iter()
20157 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20158 let tref = TableRef {
20159 name,
20160 alias: alias_ident,
20161 only: false,
20162 as_of_segment: None,
20163 unnest_expr: None,
20164 unnest_column_aliases,
20165 with_ordinality,
20166 generate_series_args: None,
20167 lateral_subquery: None,
20168 jsonb_each_text_arg: None,
20169 table_fn_call: None,
20170 rows_from: Some(generic),
20171 json_table: None,
20172 scalar_fn_item: false,
20173 };
20174 return Ok(if correlated {
20175 Self::wrap_correlated_srf(tref)
20176 } else {
20177 tref
20178 });
20179 }
20180 let correlated = entries.iter().any(Self::expr_has_any_column);
20181 let expr = if entries.len() == 1 {
20182 entries.pop().expect("len checked")
20183 } else {
20184 crate::ast::Expr::FunctionCall {
20185 name: "__unnest_zip".to_string(),
20186 args: entries,
20187 }
20188 };
20189 let tref = TableRef {
20190 name,
20191 alias: alias_ident,
20192 only: false,
20193 as_of_segment: None,
20194 unnest_expr: Some(Box::new(expr)),
20195 unnest_column_aliases,
20196 with_ordinality,
20197 generate_series_args: None,
20198 lateral_subquery: None,
20199 jsonb_each_text_arg: None,
20200 table_fn_call: None,
20201 rows_from: None,
20202 json_table: None,
20203 scalar_fn_item: false,
20204 };
20205 return Ok(if correlated {
20206 Self::wrap_correlated_srf(tref)
20207 } else {
20208 tref
20209 });
20210 }
20211 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20212 // source. Detect at the head before the bare-ident fallback;
20213 // unnest is not a reserved token.
20214 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20215 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20216 {
20217 self.advance(); // unnest
20218 self.advance(); // (
20219 let mut srf_args = alloc::vec![self.parse_expr(0)?];
20220 while matches!(self.peek(), Token::Comma) {
20221 self.advance();
20222 srf_args.push(self.parse_expr(0)?);
20223 }
20224 if !matches!(self.peek(), Token::RParen) {
20225 return Err(self.err(alloc::format!(
20226 "expected ')' after unnest() argument, got {:?}",
20227 self.peek()
20228 )));
20229 }
20230 self.advance();
20231 // Multi-arg unnest(a, b, …) zips the arrays in
20232 // parallel, NULL-padding to the longest (PG's ROWS
20233 // FROM shorthand). Lower onto the unnest channel as an
20234 // internal marker call the executors unpack.
20235 let expr = if srf_args.len() == 1 {
20236 srf_args.pop().expect("len checked")
20237 } else {
20238 crate::ast::Expr::FunctionCall {
20239 name: "__unnest_zip".to_string(),
20240 args: srf_args,
20241 }
20242 };
20243 let with_ordinality = self.absorb_with_ordinality();
20244 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20245 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20246 let correlated = Self::expr_has_any_column(&expr);
20247 let tref = TableRef {
20248 name,
20249 alias: alias_ident,
20250 only: false,
20251 as_of_segment: None,
20252 unnest_expr: Some(Box::new(expr)),
20253 unnest_column_aliases,
20254 with_ordinality,
20255 generate_series_args: None,
20256 lateral_subquery: None,
20257 jsonb_each_text_arg: None,
20258 table_fn_call: None,
20259 rows_from: None,
20260 json_table: None,
20261 scalar_fn_item: false,
20262 };
20263 return Ok(if correlated {
20264 Self::wrap_correlated_srf(tref)
20265 } else {
20266 tref
20267 });
20268 }
20269 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20270 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20271 // generic table-fn arg parser can't read), so it is intercepted
20272 // here BEFORE the generic dispatch. The doc expr may reference
20273 // outer columns (implicit LATERAL) — same correlated-wrap rule.
20274 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20275 if s.eq_ignore_ascii_case("json_table"))
20276 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20277 {
20278 let tref = self.parse_json_table_ref()?;
20279 let correlated = tref
20280 .json_table
20281 .as_deref()
20282 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20283 return Ok(if correlated {
20284 Self::wrap_correlated_srf(tref)
20285 } else {
20286 tref
20287 });
20288 }
20289 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20290 // functions dispatched by name (`pg_partition_tree('t')`,
20291 // `pg_partition_ancestors('t')`). Same head-detection shape as
20292 // unnest; the engine executor owns the row shape per function.
20293 // v7.39 (read01 round 65) — and a USER function in FROM position
20294 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20295 // (generate_series / unnest / the json_each family) keep it — their arms
20296 // sit further down, so they are excluded here by name rather than by
20297 // ordering. Anything else that is an ident followed by `(` is a table
20298 // function; the engine executor decides whether it is a builtin, a
20299 // set-returning user function, or an error.
20300 // 7.38.1 S5.1 — pg_dump spells its table functions
20301 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20302 // strip the pg_catalog prefix here so the same head-detection
20303 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20304 // meaning.
20305 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20306 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20307 && matches!(
20308 self.tokens.get(self.pos + 2),
20309 Some(Token::Ident(_) | Token::QuotedIdent(_))
20310 )
20311 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20312 {
20313 self.advance(); // pg_catalog
20314 self.advance(); // .
20315 }
20316 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20317 if !s.eq_ignore_ascii_case("generate_series")
20318 && !s.eq_ignore_ascii_case("unnest")
20319 && !is_json_each_name(s))
20320 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20321 {
20322 // Body out-of-line — this parse sits on the FROM/subquery
20323 // recursion chain (debug frame-cliff discipline).
20324 // v7.39 (read01 round 69) — a call whose arguments reference an outer
20325 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20326 // outer row, so it rides the lateral channel. Same rule the unnest
20327 // arm uses.
20328 let tref = self.parse_table_fn_ref()?;
20329 let correlated = tref
20330 .table_fn_call
20331 .as_deref()
20332 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20333 return Ok(if correlated {
20334 Self::wrap_correlated_srf(tref)
20335 } else {
20336 tref
20337 });
20338 }
20339 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20340 // [, step])` set-returning source. Same shape as unnest:
20341 // detect at the head, parse the comma-separated arg list,
20342 // dispatch downstream through the engine's set-returning
20343 // path. Supports integer triplets (mailrs's `WITH row_no AS
20344 // (SELECT * FROM generate_series(1, N))` pattern) and
20345 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20346 // date-range iteration pattern, which pre-3.10 had no
20347 // direct equivalent in SPG).
20348 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20349 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20350 {
20351 self.advance(); // generate_series
20352 self.advance(); // (
20353 let mut args: Vec<Expr> = Vec::new();
20354 loop {
20355 args.push(self.parse_expr(0)?);
20356 if matches!(self.peek(), Token::Comma) {
20357 self.advance();
20358 continue;
20359 }
20360 break;
20361 }
20362 if !matches!(self.peek(), Token::RParen) {
20363 return Err(self.err(alloc::format!(
20364 "expected ')' after generate_series() arguments, got {:?}",
20365 self.peek()
20366 )));
20367 }
20368 self.advance();
20369 if args.len() < 2 || args.len() > 3 {
20370 return Err(self.err(alloc::format!(
20371 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20372 args.len()
20373 )));
20374 }
20375 let with_ordinality = self.absorb_with_ordinality();
20376 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20377 let name = alias_ident
20378 .clone()
20379 .unwrap_or_else(|| "generate_series".to_string());
20380 let correlated = args.iter().any(Self::expr_has_any_column);
20381 let tref = TableRef {
20382 name,
20383 alias: alias_ident,
20384 only: false,
20385 as_of_segment: None,
20386 unnest_expr: None,
20387 unnest_column_aliases: column_aliases,
20388 with_ordinality,
20389 generate_series_args: Some(args),
20390 lateral_subquery: None,
20391 jsonb_each_text_arg: None,
20392 table_fn_call: None,
20393 rows_from: None,
20394 json_table: None,
20395 scalar_fn_item: false,
20396 };
20397 return Ok(if correlated {
20398 Self::wrap_correlated_srf(tref)
20399 } else {
20400 tref
20401 });
20402 }
20403 // v7.16.2 — preserve information_schema / pg_catalog
20404 // qualifiers (mailrs round-10 A.3). The generic
20405 // `expect_ident_like` strip silently drops the schema;
20406 // we want the engine to recognise these PG meta tables
20407 // and synthesise rows from the live catalog. Produce a
20408 // synthetic name (`__spg_info_columns` etc.) so the
20409 // engine's SELECT-side router can dispatch without
20410 // clashing with any user-defined `columns` table.
20411 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20412 (synth, Some(orig))
20413 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20414 (synth, Some(orig))
20415 } else {
20416 (self.expect_ident_like()?, None)
20417 };
20418 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20419 // time-travel clause. Parse BEFORE the alias so the
20420 // alias can still ride at the tail (`tbl AS OF SEGMENT
20421 // '5' alias`). `AS` is a reserved keyword token, while
20422 // `OF` and `SEGMENT` are bare idents.
20423 let as_of_segment = if matches!(self.peek(), Token::As)
20424 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20425 {
20426 self.advance(); // AS
20427 self.advance(); // OF
20428 let kw = match self.peek().clone() {
20429 Token::Ident(s) | Token::QuotedIdent(s) => s,
20430 other => {
20431 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20432 }
20433 };
20434 if !kw.eq_ignore_ascii_case("segment") {
20435 return Err(self.err(format!(
20436 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20437 )));
20438 }
20439 self.advance();
20440 // Segment id literal — accept either a string or
20441 // integer for operator ergonomics.
20442 let id = match self.advance() {
20443 Token::String(s) => s
20444 .parse::<u32>()
20445 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20446 Token::Integer(n) => u32::try_from(n)
20447 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20448 other => {
20449 return Err(self.err(format!(
20450 "expected segment id literal after AS OF SEGMENT, got {other:?}"
20451 )));
20452 }
20453 };
20454 Some(id)
20455 } else {
20456 None
20457 };
20458 // TABLESAMPLE is not a reserved token — keep the bare-ident
20459 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20460 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20461 {
20462 None
20463 } else {
20464 self.parse_optional_alias()?
20465 };
20466 // r1052 — a catalog name rewritten to its synthetic form keeps
20467 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20468 // still binds after `pg_cast` became `__spg_pg_cast`. PG
20469 // semantics: the visible name of `pg_catalog.pg_cast` IS
20470 // `pg_cast`. Without this, every table-name-qualified column
20471 // on a synthesised catalog answered "missing FROM-clause
20472 // entry" — which is the wall pg_dump hit on its first
20473 // pg_proc/pg_cast query.
20474 let alias = match (&alias, &meta_original) {
20475 (None, Some(orig)) if *orig != name => Some(orig.clone()),
20476 _ => alias,
20477 };
20478 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20479 // (PG grammar). BERNOULLI lowers to a per-row
20480 // `random() < p/100` conjunct on the enclosing SELECT's
20481 // WHERE — exact row-level Bernoulli semantics. SYSTEM
20482 // shares the lowering: SPG has no page structure to
20483 // sample, and the row-level form returns the same expected
20484 // fraction. REPEATABLE(seed) promises a deterministic
20485 // sample SPG cannot honour yet — honest error rather than
20486 // a silently ignored seed.
20487 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20488 self.advance();
20489 let method = self.expect_ident_like()?;
20490 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20491 return Err(self.err(alloc::format!(
20492 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20493 )));
20494 }
20495 if !matches!(self.peek(), Token::LParen) {
20496 return Err(self.err(alloc::format!(
20497 "expected '(' after TABLESAMPLE {}, got {:?}",
20498 method.to_ascii_uppercase(),
20499 self.peek()
20500 )));
20501 }
20502 self.advance();
20503 let percent = self.parse_expr(0)?;
20504 if !matches!(self.peek(), Token::RParen) {
20505 return Err(self.err(alloc::format!(
20506 "expected ')' after TABLESAMPLE percentage, got {:?}",
20507 self.peek()
20508 )));
20509 }
20510 self.advance();
20511 // REPEATABLE(seed) → a deterministic per-row draw seeded by
20512 // `seed`, so the sample is stable across repeats and rescans.
20513 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20514 let mut sample_seed: Option<Expr> = None;
20515 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20516 self.advance();
20517 if !matches!(self.peek(), Token::LParen) {
20518 return Err(self.err(alloc::format!(
20519 "expected '(' after REPEATABLE, got {:?}",
20520 self.peek()
20521 )));
20522 }
20523 self.advance();
20524 let seed = self.parse_expr(0)?;
20525 if !matches!(self.peek(), Token::RParen) {
20526 return Err(self.err(alloc::format!(
20527 "expected ')' after REPEATABLE seed, got {:?}",
20528 self.peek()
20529 )));
20530 }
20531 self.advance();
20532 sample_seed = Some(seed);
20533 }
20534 let draw = match sample_seed {
20535 Some(seed) => Expr::FunctionCall {
20536 name: "__tsm_fract".to_string(),
20537 args: alloc::vec![seed],
20538 },
20539 None => Expr::FunctionCall {
20540 name: "random".to_string(),
20541 args: Vec::new(),
20542 },
20543 };
20544 self.pending_sample_preds.push(Expr::Binary {
20545 lhs: Box::new(draw),
20546 op: crate::ast::BinOp::Lt,
20547 rhs: Box::new(Expr::Binary {
20548 lhs: Box::new(percent),
20549 op: crate::ast::BinOp::Div,
20550 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20551 }),
20552 });
20553 }
20554 Ok(TableRef {
20555 name,
20556 alias,
20557 only,
20558 as_of_segment,
20559 unnest_expr: None,
20560 unnest_column_aliases: Vec::new(),
20561 with_ordinality: false,
20562 generate_series_args: None,
20563 lateral_subquery: None,
20564 jsonb_each_text_arg: None,
20565 table_fn_call: None,
20566 rows_from: None,
20567 json_table: None,
20568 scalar_fn_item: false,
20569 })
20570 }
20571
20572 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20573 /// but also accepts `AS alias(col [, col, …])` — the
20574 /// PG-standard table-function column-list form. The column
20575 /// list is only honoured when paired with `UNNEST(...)` in
20576 /// the parent; other call sites currently discard it.
20577 /// True when the expression tree contains a qualified column
20578 /// reference (`t.col`) — the syntactic marker that an SRF
20579 /// argument correlates with a preceding FROM item.
20580 fn expr_has_qualified_column(e: &Expr) -> bool {
20581 match e {
20582 Expr::Column(c) => c.qualifier.is_some(),
20583 Expr::Binary { lhs, rhs, .. } => {
20584 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20585 }
20586 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20587 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20588 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20589 Expr::Case {
20590 operand,
20591 branches,
20592 else_branch,
20593 } => {
20594 operand
20595 .as_deref()
20596 .is_some_and(Self::expr_has_qualified_column)
20597 || branches.iter().any(|(w, t)| {
20598 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20599 })
20600 || else_branch
20601 .as_deref()
20602 .is_some_and(Self::expr_has_qualified_column)
20603 }
20604 _ => false,
20605 }
20606 }
20607
20608 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20609 /// counts a bare (unqualified) column. A set-returning function has no
20610 /// input columns of its own, so ANY column in its arguments is an outer
20611 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20612 fn expr_has_any_column(e: &Expr) -> bool {
20613 match e {
20614 Expr::Column(_) => true,
20615 Expr::Binary { lhs, rhs, .. } => {
20616 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20617 }
20618 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20619 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20620 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20621 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20622 // constructor or subscript fell to the `_ => false` arm, so
20623 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20624 // channel and the eager peer eval answered `column "x" does
20625 // not exist` (the substitution walker already recurses both
20626 // shapes; only this detector was blind to them).
20627 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20628 Expr::ArraySubscript { target, index } => {
20629 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20630 }
20631 Expr::Case {
20632 operand,
20633 branches,
20634 else_branch,
20635 } => {
20636 operand.as_deref().is_some_and(Self::expr_has_any_column)
20637 || branches
20638 .iter()
20639 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20640 || else_branch
20641 .as_deref()
20642 .is_some_and(Self::expr_has_any_column)
20643 }
20644 _ => false,
20645 }
20646 }
20647
20648 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20649 /// `generate_series(1, t.n)`) into the lateral_subquery
20650 /// channel: `SELECT * FROM <srf>` executes per outer row with
20651 /// outer references substituted (v7.37.43-T4.5 machinery).
20652 /// Uncorrelated SRFs stay on their plain channels.
20653 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20654 let name = srf.name.clone();
20655 let alias = srf.alias.clone();
20656 let inner = crate::ast::SelectStatement {
20657 locking: None,
20658 ctes: Vec::new(),
20659 distinct: false,
20660 distinct_on: Vec::new(),
20661 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20662 from: Some(crate::ast::FromClause {
20663 primary: srf,
20664 joins: Vec::new(),
20665 }),
20666 where_: None,
20667 group_by: None,
20668 group_by_all: false,
20669 having: None,
20670 unions: Vec::new(),
20671 order_by: Vec::new(),
20672 limit: None,
20673 offset: None,
20674 limit_with_ties: false,
20675 window_check_exprs: Vec::new(),
20676 };
20677 TableRef {
20678 name,
20679 alias,
20680 only: false,
20681 as_of_segment: None,
20682 unnest_expr: None,
20683 unnest_column_aliases: Vec::new(),
20684 with_ordinality: false,
20685 generate_series_args: None,
20686 lateral_subquery: Some(Box::new(inner)),
20687 jsonb_each_text_arg: None,
20688 table_fn_call: None,
20689 rows_from: None,
20690 json_table: None,
20691 scalar_fn_item: false,
20692 }
20693 }
20694
20695 /// True when the expression tree contains an unresolved
20696 /// `OVER w` marker (see parse_over_clause).
20697 fn expr_has_named_window(e: &Expr) -> bool {
20698 match e {
20699 Expr::WindowFunction { partition_by, .. } => matches!(
20700 partition_by.as_slice(),
20701 [Expr::Column(c)] if matches!(
20702 c.qualifier.as_deref(),
20703 Some("__named_window__") | Some("__named_window_ref__")
20704 )
20705 ),
20706 Expr::Binary { lhs, rhs, .. } => {
20707 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20708 }
20709 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20710 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20711 Expr::Case {
20712 operand,
20713 branches,
20714 else_branch,
20715 } => {
20716 operand.as_deref().is_some_and(Self::expr_has_named_window)
20717 || branches.iter().any(|(w, t)| {
20718 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20719 })
20720 || else_branch
20721 .as_deref()
20722 .is_some_and(Self::expr_has_named_window)
20723 }
20724 _ => false,
20725 }
20726 }
20727
20728 /// v7.39 (round 705) — the NAMES the expression references through the
20729 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20730 /// definitions nothing referenced. Traversal mirrors
20731 /// `expr_has_named_window` above.
20732 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20733 match e {
20734 Expr::WindowFunction { partition_by, .. } => {
20735 if let [Expr::Column(c)] = partition_by.as_slice()
20736 && matches!(
20737 c.qualifier.as_deref(),
20738 Some("__named_window__") | Some("__named_window_ref__")
20739 )
20740 {
20741 into.push(c.name.clone());
20742 }
20743 }
20744 Expr::Binary { lhs, rhs, .. } => {
20745 Self::collect_named_window_refs(lhs, into);
20746 Self::collect_named_window_refs(rhs, into);
20747 }
20748 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20749 Self::collect_named_window_refs(expr, into);
20750 }
20751 Expr::FunctionCall { args, .. } => {
20752 for a in args {
20753 Self::collect_named_window_refs(a, into);
20754 }
20755 }
20756 Expr::Case {
20757 operand,
20758 branches,
20759 else_branch,
20760 } => {
20761 if let Some(o) = operand.as_deref() {
20762 Self::collect_named_window_refs(o, into);
20763 }
20764 for (w, t) in branches {
20765 Self::collect_named_window_refs(w, into);
20766 Self::collect_named_window_refs(t, into);
20767 }
20768 if let Some(eb) = else_branch.as_deref() {
20769 Self::collect_named_window_refs(eb, into);
20770 }
20771 }
20772 _ => {}
20773 }
20774 }
20775
20776 /// Inline named-window definitions into the `OVER w` markers.
20777 /// An unknown name errors (PG: window "w" does not exist).
20778 #[allow(clippy::type_complexity)]
20779 fn substitute_named_windows(
20780 e: &mut Expr,
20781 defs: &[(
20782 String,
20783 (
20784 Vec<Expr>,
20785 Vec<(Expr, bool, Option<bool>)>,
20786 Option<WindowFrame>,
20787 ),
20788 )],
20789 ) -> Result<(), String> {
20790 match e {
20791 Expr::WindowFunction {
20792 partition_by,
20793 order_by,
20794 frame,
20795 ..
20796 } => {
20797 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20798 // from the bare `OVER w1` (a plain reference).
20799 let named = match partition_by.as_slice() {
20800 [Expr::Column(c)] => match c.qualifier.as_deref() {
20801 Some("__named_window__") => Some((c.name.clone(), false)),
20802 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20803 _ => None,
20804 },
20805 _ => None,
20806 };
20807 if let Some((wname, is_copy)) = named {
20808 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20809 else {
20810 return Err(alloc::format!("window {wname:?} does not exist"));
20811 };
20812 if !is_copy {
20813 *partition_by = def.0.clone();
20814 *order_by = def.1.clone();
20815 *frame = def.2.clone();
20816 return Ok(());
20817 }
20818 // v7.39 (round 229) — PG's copy rules, probed against
20819 // 18.4: a copy inherits the partitioning, may supply an
20820 // ordering only when the base has none, and may not copy
20821 // a base that already carries a frame (its own frame
20822 // would be ambiguous with the inherited one).
20823 if !def.1.is_empty() && !order_by.is_empty() {
20824 return Err(alloc::format!(
20825 "cannot override ORDER BY clause of window \"{wname}\""
20826 ));
20827 }
20828 if def.2.is_some() {
20829 return Err(alloc::format!(
20830 "cannot copy window \"{wname}\" because it has a frame clause"
20831 ));
20832 }
20833 *partition_by = def.0.clone();
20834 if order_by.is_empty() {
20835 *order_by = def.1.clone();
20836 }
20837 }
20838 Ok(())
20839 }
20840 Expr::Binary { lhs, rhs, .. } => {
20841 Self::substitute_named_windows(lhs, defs)?;
20842 Self::substitute_named_windows(rhs, defs)
20843 }
20844 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20845 Self::substitute_named_windows(expr, defs)
20846 }
20847 Expr::FunctionCall { args, .. } => {
20848 for a in args {
20849 Self::substitute_named_windows(a, defs)?;
20850 }
20851 Ok(())
20852 }
20853 Expr::Case {
20854 operand,
20855 branches,
20856 else_branch,
20857 } => {
20858 if let Some(op) = operand {
20859 Self::substitute_named_windows(op, defs)?;
20860 }
20861 for (w, t) in branches {
20862 Self::substitute_named_windows(w, defs)?;
20863 Self::substitute_named_windows(t, defs)?;
20864 }
20865 if let Some(el) = else_branch {
20866 Self::substitute_named_windows(el, defs)?;
20867 }
20868 Ok(())
20869 }
20870 _ => Ok(()),
20871 }
20872 }
20873
20874 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20875 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20876 /// composition.
20877 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20878 debug_assert!(matches!(self.peek(), Token::Table));
20879 self.advance(); // TABLE
20880 let tname = self.expect_ident_like()?;
20881 Ok(SelectStatement {
20882 locking: None,
20883 ctes: Vec::new(),
20884 distinct: false,
20885 distinct_on: Vec::new(),
20886 items: alloc::vec![SelectItem::Wildcard],
20887 from: Some(FromClause {
20888 primary: TableRef {
20889 name: tname,
20890 alias: None,
20891 only: false,
20892 as_of_segment: None,
20893 unnest_expr: None,
20894 unnest_column_aliases: Vec::new(),
20895 with_ordinality: false,
20896 generate_series_args: None,
20897 lateral_subquery: None,
20898 jsonb_each_text_arg: None,
20899 table_fn_call: None,
20900 rows_from: None,
20901 json_table: None,
20902 scalar_fn_item: false,
20903 },
20904 joins: Vec::new(),
20905 }),
20906 where_: None,
20907 group_by: None,
20908 group_by_all: false,
20909 having: None,
20910 unions: Vec::new(),
20911 order_by: Vec::new(),
20912 limit: None,
20913 offset: None,
20914 limit_with_ties: false,
20915 window_check_exprs: Vec::new(),
20916 })
20917 }
20918
20919 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20920 /// variants) → a derived table that reads each declared column out of
20921 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20922 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20923 /// the scalar *record form projects a single row straight off `J`.
20924 /// Rides the existing lateral-subquery channel, so no new executor or
20925 /// AST is needed.
20926 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20927 use crate::ast::{
20928 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20929 };
20930 let fn_name = match self.peek() {
20931 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20932 _ => unreachable!("caller guarded is_json_to_record_name"),
20933 };
20934 self.advance(); // fn name
20935 self.advance(); // (
20936 let mut arg = self.parse_expr(0)?;
20937 // populate_record(base, json): the base only carries the record
20938 // type here — the JSON argument is the second expression.
20939 let mut base: Option<Expr> = None;
20940 if matches!(self.peek(), Token::Comma) {
20941 self.advance();
20942 base = Some(arg);
20943 arg = self.parse_expr(0)?;
20944 }
20945 if !matches!(self.peek(), Token::RParen) {
20946 return Err(self.err(alloc::format!(
20947 "expected ')' after {fn_name}() argument, got {:?}",
20948 self.peek()
20949 )));
20950 }
20951 self.advance(); // )
20952 let is_set = fn_name.ends_with("recordset");
20953 // `[AS] alias ( col type [, …] )` column-definition list.
20954 if matches!(self.peek(), Token::As) {
20955 self.advance();
20956 }
20957 let alias_opt = match self.peek() {
20958 Token::Ident(s) | Token::QuotedIdent(s) => {
20959 let a = s.clone();
20960 self.advance();
20961 Some(a)
20962 }
20963 _ => None,
20964 };
20965 // v7.39 (read01 round 76) — the populate family's canonical PG
20966 // spelling carries no column list at all: the row shape comes from
20967 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20968 // j)`). The parser has no catalog, so hand the two arguments to the
20969 // engine's table-function channel, which does. Only `*_to_record*`
20970 // (whose base is bare `record`) genuinely requires the list.
20971 if !matches!(self.peek(), Token::LParen) {
20972 if let Some(base_expr) = base {
20973 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20974 return Ok(TableRef {
20975 name: alias.clone(),
20976 alias: Some(alias),
20977 only: false,
20978 as_of_segment: None,
20979 unnest_expr: None,
20980 unnest_column_aliases: Vec::new(),
20981 with_ordinality: false,
20982 generate_series_args: None,
20983 lateral_subquery: None,
20984 jsonb_each_text_arg: None,
20985 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20986 rows_from: None,
20987 json_table: None,
20988 scalar_fn_item: false,
20989 });
20990 }
20991 return Err(self.err(alloc::format!(
20992 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20993 self.peek()
20994 )));
20995 }
20996 let Some(alias) = alias_opt else {
20997 return Err(self.err(alloc::format!(
20998 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20999 )));
21000 };
21001 self.advance(); // (
21002 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
21003 loop {
21004 let col = self.expect_ident_like()?;
21005 let ty = self.parse_cast_target()?;
21006 coldefs.push((col, ty));
21007 if matches!(self.peek(), Token::Comma) {
21008 self.advance();
21009 continue;
21010 }
21011 if matches!(self.peek(), Token::RParen) {
21012 self.advance();
21013 break;
21014 }
21015 return Err(self.err(alloc::format!(
21016 "expected ',' or ')' in {fn_name} column list, got {:?}",
21017 self.peek()
21018 )));
21019 }
21020 if coldefs.is_empty() {
21021 return Err(self.err(alloc::format!(
21022 "{fn_name} column-definition list must declare at least one column"
21023 )));
21024 }
21025 // Per column: (base ->> 'col')::type AS col. The base is the
21026 // per-element `value` column for the *set form, or the argument
21027 // itself for the scalar record form.
21028 let items: Vec<SelectItem> = coldefs
21029 .into_iter()
21030 .map(|(col, ty)| {
21031 let base = if is_set {
21032 Expr::Column(ColumnName {
21033 qualifier: None,
21034 name: "value".to_string(),
21035 })
21036 } else {
21037 arg.clone()
21038 };
21039 SelectItem::Expr {
21040 expr: Expr::Cast {
21041 expr: Box::new(Expr::Binary {
21042 lhs: Box::new(base),
21043 op: BinOp::JsonGetText,
21044 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21045 }),
21046 target: ty,
21047 },
21048 alias: Some(col),
21049 }
21050 })
21051 .collect();
21052 let from = if is_set {
21053 let elem_fn = if fn_name.starts_with("jsonb") {
21054 "jsonb_array_elements"
21055 } else {
21056 "json_array_elements"
21057 };
21058 Some(FromClause {
21059 primary: TableRef {
21060 name: "value".to_string(),
21061 alias: None,
21062 only: false,
21063 as_of_segment: None,
21064 unnest_expr: Some(Box::new(Expr::FunctionCall {
21065 name: elem_fn.to_string(),
21066 args: alloc::vec![arg],
21067 })),
21068 unnest_column_aliases: alloc::vec!["value".to_string()],
21069 with_ordinality: false,
21070 generate_series_args: None,
21071 lateral_subquery: None,
21072 jsonb_each_text_arg: None,
21073 table_fn_call: None,
21074 rows_from: None,
21075 json_table: None,
21076 scalar_fn_item: false,
21077 },
21078 joins: Vec::new(),
21079 })
21080 } else {
21081 None
21082 };
21083 let inner = SelectStatement {
21084 locking: None,
21085 ctes: Vec::new(),
21086 distinct: false,
21087 distinct_on: Vec::new(),
21088 items,
21089 from,
21090 where_: None,
21091 group_by: None,
21092 group_by_all: false,
21093 having: None,
21094 unions: Vec::new(),
21095 order_by: Vec::new(),
21096 limit: None,
21097 offset: None,
21098 limit_with_ties: false,
21099 window_check_exprs: Vec::new(),
21100 };
21101 Ok(TableRef {
21102 name: alias.clone(),
21103 alias: Some(alias),
21104 only: false,
21105 as_of_segment: None,
21106 unnest_expr: None,
21107 unnest_column_aliases: Vec::new(),
21108 with_ordinality: false,
21109 generate_series_args: None,
21110 lateral_subquery: Some(Box::new(inner)),
21111 jsonb_each_text_arg: None,
21112 table_fn_call: None,
21113 rows_from: None,
21114 json_table: None,
21115 scalar_fn_item: false,
21116 })
21117 }
21118
21119 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21120 /// Returns true when the clause was present. `WITH` alone (a
21121 /// CTE can never start here) is not enough — the ORDINALITY
21122 /// ident must follow, so a stray WITH still errors downstream.
21123 fn absorb_with_ordinality(&mut self) -> bool {
21124 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21125 && matches!(self.tokens.get(self.pos + 1),
21126 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21127 {
21128 self.advance();
21129 self.advance();
21130 true
21131 } else {
21132 false
21133 }
21134 }
21135
21136 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21137 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21138 /// Out-of-line: the caller sits on the FROM recursion chain.
21139 #[inline(never)]
21140 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21141 let fn_name = match self.advance() {
21142 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21143 _ => unreachable!("caller peeked an ident"),
21144 };
21145 self.advance(); // (
21146 let mut args: Vec<Expr> = Vec::new();
21147 if !matches!(self.peek(), Token::RParen) {
21148 loop {
21149 args.push(self.parse_expr(0)?);
21150 if matches!(self.peek(), Token::Comma) {
21151 self.advance();
21152 continue;
21153 }
21154 break;
21155 }
21156 }
21157 if !matches!(self.peek(), Token::RParen) {
21158 return Err(self.err(alloc::format!(
21159 "expected ')' after {fn_name}() arguments, got {:?}",
21160 self.peek()
21161 )));
21162 }
21163 self.advance();
21164 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21165 // counter column rides after the function's own, and the alias list
21166 // names it.
21167 let with_ordinality = self.absorb_with_ordinality();
21168 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21169 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21170 Ok(TableRef {
21171 name,
21172 alias: alias_ident,
21173 only: false,
21174 as_of_segment: None,
21175 unnest_expr: None,
21176 unnest_column_aliases,
21177 with_ordinality,
21178 generate_series_args: None,
21179 lateral_subquery: None,
21180 jsonb_each_text_arg: None,
21181 table_fn_call: Some(Box::new((fn_name, args))),
21182 rows_from: None,
21183 json_table: None,
21184 scalar_fn_item: false,
21185 })
21186 }
21187
21188 /// v7.39 (round 205, JSON_TABLE) — parse
21189 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21190 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21191 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21192 #[inline(never)]
21193 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21194 self.advance(); // json_table
21195 self.advance(); // (
21196 let doc = Box::new(self.parse_expr(0)?);
21197 self.expect_comma_json_table()?;
21198 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21199 // Optional `PASSING <expr> AS <name> [, …]`.
21200 let mut passing: Vec<(String, Expr)> = Vec::new();
21201 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21202 self.advance();
21203 loop {
21204 let e = self.parse_expr(0)?;
21205 if !matches!(self.peek(), Token::As) {
21206 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21207 }
21208 self.advance();
21209 let vname = match self.advance() {
21210 Token::Ident(s) | Token::QuotedIdent(s) => s,
21211 other => {
21212 return Err(self.err(alloc::format!(
21213 "expected PASSING variable name, got {other:?}"
21214 )));
21215 }
21216 };
21217 passing.push((vname, e));
21218 if matches!(self.peek(), Token::Comma) {
21219 self.advance();
21220 continue;
21221 }
21222 break;
21223 }
21224 }
21225 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21226 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21227 }
21228 self.advance();
21229 let columns = self.parse_json_table_columns()?;
21230 if !matches!(self.peek(), Token::RParen) {
21231 return Err(self.err(alloc::format!(
21232 "expected ')' to close JSON_TABLE, got {:?}",
21233 self.peek()
21234 )));
21235 }
21236 self.advance();
21237 let alias_ident = self.parse_optional_alias()?;
21238 let name = alias_ident
21239 .clone()
21240 .unwrap_or_else(|| String::from("json_table"));
21241 Ok(TableRef {
21242 name,
21243 alias: alias_ident,
21244 only: false,
21245 as_of_segment: None,
21246 unnest_expr: None,
21247 unnest_column_aliases: Vec::new(),
21248 with_ordinality: false,
21249 generate_series_args: None,
21250 lateral_subquery: None,
21251 jsonb_each_text_arg: None,
21252 table_fn_call: None,
21253 rows_from: None,
21254 json_table: Some(Box::new(crate::ast::JsonTable {
21255 doc,
21256 row_path,
21257 columns,
21258 passing,
21259 })),
21260 scalar_fn_item: false,
21261 })
21262 }
21263
21264 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21265 if !matches!(self.peek(), Token::Comma) {
21266 return Err(self.err(alloc::format!(
21267 "expected ',' after JSON_TABLE document, got {:?}",
21268 self.peek()
21269 )));
21270 }
21271 self.advance();
21272 Ok(())
21273 }
21274
21275 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21276 match self.advance() {
21277 Token::String(s) => Ok(s),
21278 other => Err(self.err(alloc::format!(
21279 "expected {what} string literal, got {other:?}"
21280 ))),
21281 }
21282 }
21283
21284 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21285 #[inline(never)]
21286 fn parse_json_table_columns(
21287 &mut self,
21288 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21289 if !matches!(self.peek(), Token::LParen) {
21290 return Err(self.err("expected '(' after COLUMNS".into()));
21291 }
21292 self.advance();
21293 let mut cols = Vec::new();
21294 loop {
21295 cols.push(self.parse_json_table_one_column()?);
21296 if matches!(self.peek(), Token::Comma) {
21297 self.advance();
21298 continue;
21299 }
21300 break;
21301 }
21302 if !matches!(self.peek(), Token::RParen) {
21303 return Err(self.err(alloc::format!(
21304 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21305 self.peek()
21306 )));
21307 }
21308 self.advance();
21309 Ok(cols)
21310 }
21311
21312 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21313 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21314 // NESTED [PATH] '<p>' COLUMNS (...)
21315 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21316 self.advance();
21317 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21318 self.advance();
21319 }
21320 let path = self.parse_json_string_literal("NESTED PATH")?;
21321 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21322 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21323 }
21324 self.advance();
21325 let columns = self.parse_json_table_columns()?;
21326 return Ok(JsonTableColumn::Nested { path, columns });
21327 }
21328 // <name> ...
21329 let name = match self.advance() {
21330 Token::Ident(s) | Token::QuotedIdent(s) => s,
21331 other => {
21332 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21333 }
21334 };
21335 // <name> FOR ORDINALITY
21336 if matches!(self.peek(), Token::For) {
21337 self.advance();
21338 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21339 return Err(self.err("expected ORDINALITY after FOR".into()));
21340 }
21341 self.advance();
21342 return Ok(JsonTableColumn::Ordinality { name });
21343 }
21344 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21345 let ty = self.parse_column_type_name()?;
21346 let mut format_json = false;
21347 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21348 self.advance();
21349 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21350 return Err(self.err("expected JSON after FORMAT".into()));
21351 }
21352 self.advance();
21353 format_json = true;
21354 }
21355 let mut exists = false;
21356 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21357 self.advance();
21358 exists = true;
21359 }
21360 let mut path = alloc::format!("$.{name}");
21361 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21362 self.advance();
21363 path = self.parse_json_string_literal("column PATH")?;
21364 }
21365 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21366 // `FORMAT JSON` after PATH (alternate placement).
21367 self.advance();
21368 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21369 self.advance();
21370 }
21371 format_json = true;
21372 }
21373 let mut wrapper = false;
21374 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21375 self.advance();
21376 // optional CONDITIONAL/UNCONDITIONAL
21377 if matches!(self.peek(), Token::Ident(s)
21378 if s.eq_ignore_ascii_case("unconditional")
21379 || s.eq_ignore_ascii_case("conditional"))
21380 {
21381 self.advance();
21382 }
21383 if !matches!(self.peek(), Token::Ident(s)
21384 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21385 {
21386 return Err(self.err("expected WRAPPER after WITH".into()));
21387 }
21388 self.advance();
21389 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21390 if matches!(self.peek(), Token::Ident(s)
21391 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21392 {
21393 self.advance();
21394 }
21395 wrapper = true;
21396 }
21397 // ON EMPTY / ON ERROR clauses (two, in any order).
21398 let mut on_empty = JsonTableOnBehavior::Null;
21399 let mut on_error = JsonTableOnBehavior::Null;
21400 for _ in 0..2 {
21401 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21402 {
21403 self.advance();
21404 Some(JsonTableOnBehavior::Error)
21405 } else if matches!(self.peek(), Token::Null) {
21406 self.advance();
21407 Some(JsonTableOnBehavior::Null)
21408 } else if matches!(self.peek(), Token::Default) {
21409 self.advance();
21410 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21411 } else {
21412 None
21413 };
21414 let Some(behavior) = behavior else { break };
21415 // `ON {EMPTY|ERROR}`
21416 if !matches!(self.peek(), Token::On) {
21417 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21418 }
21419 self.advance();
21420 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21421 self.advance();
21422 on_empty = behavior;
21423 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21424 self.advance();
21425 on_error = behavior;
21426 } else {
21427 return Err(self.err("expected EMPTY or ERROR after ON".into()));
21428 }
21429 }
21430 Ok(JsonTableColumn::Regular {
21431 name,
21432 ty,
21433 path,
21434 exists,
21435 format_json,
21436 wrapper,
21437 on_empty,
21438 on_error,
21439 })
21440 }
21441
21442 fn parse_optional_alias_with_columns(
21443 &mut self,
21444 ) -> Result<(Option<String>, Vec<String>), ParseError> {
21445 let alias = self.parse_optional_alias()?;
21446 if alias.is_none() {
21447 return Ok((None, Vec::new()));
21448 }
21449 let mut cols: Vec<String> = Vec::new();
21450 if matches!(self.peek(), Token::LParen) {
21451 self.advance();
21452 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21453 self.advance();
21454 cols.push(s);
21455 if matches!(self.peek(), Token::Comma) {
21456 self.advance();
21457 continue;
21458 }
21459 break;
21460 }
21461 if matches!(self.peek(), Token::RParen) {
21462 self.advance();
21463 }
21464 }
21465 Ok((alias, cols))
21466 }
21467
21468 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21469 /// whose keyword token was already consumed and whose `(` is the
21470 /// current token. Factored out of `parse_atom` (and marked
21471 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21472 /// recursive `parse_atom` frame — inlining them there enlarges the
21473 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21474 /// against, risking an overflow before the budget triggers.
21475 #[inline(never)]
21476 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21477 self.advance(); // (
21478 let mut args = Vec::new();
21479 if !matches!(self.peek(), Token::RParen) {
21480 loop {
21481 args.push(self.parse_expr(0)?);
21482 match self.peek() {
21483 Token::Comma => {
21484 self.advance();
21485 }
21486 Token::RParen => break,
21487 other => {
21488 return Err(self.err(alloc::format!(
21489 "expected ',' or ')' in {name}() args, got {other:?}"
21490 )));
21491 }
21492 }
21493 }
21494 }
21495 self.advance(); // )
21496 Ok(Expr::FunctionCall {
21497 name: name.into(),
21498 args,
21499 })
21500 }
21501
21502 /// FROM-clause: a primary table reference plus zero-or-more joined
21503 /// peers expressed via either `, <table>` (cross-product, no ON) or
21504 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21505 /// v1.10 keeps the join list flat (left-associative nested-loop
21506 /// semantics).
21507 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21508 let primary = self.parse_table_ref()?;
21509 let primary_qual = primary
21510 .alias
21511 .clone()
21512 .unwrap_or_else(|| primary.name.clone());
21513 let joins = self.parse_from_joins(&primary_qual)?;
21514 Ok(FromClause { primary, joins })
21515 }
21516
21517 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21518 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21519 /// SAME grammar after its target table has already been consumed.
21520 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21521 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21522 /// be parsed forward, once.)
21523 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21524 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21525 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21526 /// desugaring, which needs a name for the left side of each equality.
21527 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21528 let mut joins = Vec::new();
21529 loop {
21530 // `, <table>` — cross-product with no ON.
21531 if matches!(self.peek(), Token::Comma) {
21532 self.advance();
21533 let table = self.parse_table_ref()?;
21534 joins.push(FromJoin {
21535 kind: JoinKind::Cross,
21536 table,
21537 on: None,
21538 using_cols: None,
21539 natural: false,
21540 });
21541 continue;
21542 }
21543 // v7.37.16 — optional leading `NATURAL` before the join
21544 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21545 // not a lexer keyword (it arrives as a bare Ident), so match
21546 // it case-insensitively here. When present, no ON/USING
21547 // clause is allowed — the common columns are resolved at
21548 // execution time.
21549 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21550 if natural {
21551 self.advance();
21552 }
21553 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21554 // CROSS JOIN, and bare JOIN (defaults to INNER).
21555 let kind =
21556 match self.peek() {
21557 Token::Inner => {
21558 self.advance();
21559 if !matches!(self.peek(), Token::Join) {
21560 return Err(self
21561 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21562 }
21563 self.advance();
21564 JoinKind::Inner
21565 }
21566 Token::Left => {
21567 self.advance();
21568 if matches!(self.peek(), Token::Outer) {
21569 self.advance();
21570 }
21571 if !matches!(self.peek(), Token::Join) {
21572 return Err(self.err(format!(
21573 "expected JOIN after LEFT [OUTER], got {:?}",
21574 self.peek()
21575 )));
21576 }
21577 self.advance();
21578 JoinKind::Left
21579 }
21580 Token::Cross => {
21581 self.advance();
21582 if !matches!(self.peek(), Token::Join) {
21583 return Err(self
21584 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21585 }
21586 self.advance();
21587 JoinKind::Cross
21588 }
21589 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21590 Token::Right => {
21591 self.advance();
21592 if matches!(self.peek(), Token::Outer) {
21593 self.advance();
21594 }
21595 if !matches!(self.peek(), Token::Join) {
21596 return Err(self.err(format!(
21597 "expected JOIN after RIGHT [OUTER], got {:?}",
21598 self.peek()
21599 )));
21600 }
21601 self.advance();
21602 JoinKind::Right
21603 }
21604 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21605 Token::Full => {
21606 self.advance();
21607 if matches!(self.peek(), Token::Outer) {
21608 self.advance();
21609 }
21610 if !matches!(self.peek(), Token::Join) {
21611 return Err(self.err(format!(
21612 "expected JOIN after FULL [OUTER], got {:?}",
21613 self.peek()
21614 )));
21615 }
21616 self.advance();
21617 JoinKind::FullOuter
21618 }
21619 Token::Join => {
21620 self.advance();
21621 JoinKind::Inner
21622 }
21623 _ => break,
21624 };
21625 let table = self.parse_table_ref()?;
21626 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21627 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21628 // where prev_table is the most-recent left-side table
21629 // (the previous join's table if any, else the FROM primary).
21630 // PG semantics around column merging are richer (USING'd
21631 // cols become deduplicated single output columns); for
21632 // sugar purposes the predicate-only form covers the
21633 // baseline corpus shape and chained `… JOIN x USING (k)
21634 // JOIN y USING (k)` calls.
21635 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21636 // common columns resolve at execution time.
21637 if natural {
21638 joins.push(FromJoin {
21639 kind,
21640 table,
21641 on: None,
21642 using_cols: None,
21643 natural: true,
21644 });
21645 continue;
21646 }
21647 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21648 // v7.37.16 — capture the USING column list (in addition to
21649 // the ON desugar below) so the executor can perform PG's
21650 // column-merge on the output side.
21651 let mut using_cols: Option<Vec<String>> = None;
21652 let on = if matches!(self.peek(), Token::On) {
21653 self.advance();
21654 Some(self.parse_expr(0)?)
21655 } else if using_match {
21656 self.advance();
21657 if !matches!(self.peek(), Token::LParen) {
21658 return Err(
21659 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21660 );
21661 }
21662 self.advance();
21663 let mut cols: Vec<String> = Vec::new();
21664 loop {
21665 match self.peek().clone() {
21666 Token::Ident(s) | Token::QuotedIdent(s) => {
21667 self.advance();
21668 cols.push(s);
21669 }
21670 other => {
21671 return Err(self.err(format!(
21672 "expected column name inside USING (…), got {other:?}"
21673 )));
21674 }
21675 }
21676 match self.peek() {
21677 Token::Comma => {
21678 self.advance();
21679 continue;
21680 }
21681 Token::RParen => {
21682 self.advance();
21683 break;
21684 }
21685 other => {
21686 return Err(self.err(format!(
21687 "expected ',' or ')' inside USING (…), got {other:?}"
21688 )));
21689 }
21690 }
21691 }
21692 if cols.is_empty() {
21693 return Err(self.err("USING (…) requires at least one column".to_string()));
21694 }
21695 using_cols = Some(cols.clone());
21696 // Pick the left-side alias: prev join's table if any,
21697 // else FROM primary. Use alias when present, else
21698 // table name (PG-equivalent qualifier).
21699 let left_qual: String = joins
21700 .last()
21701 .map(|j| {
21702 j.table
21703 .alias
21704 .clone()
21705 .unwrap_or_else(|| j.table.name.clone())
21706 })
21707 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21708 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21709 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21710 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21711 qualifier: Some(left_qual.clone()),
21712 name: c.clone(),
21713 })),
21714 op: crate::ast::BinOp::Eq,
21715 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21716 qualifier: Some(right_qual.clone()),
21717 name: c,
21718 })),
21719 });
21720 let first = iter.next().expect("at least one col");
21721 Some(iter.fold(first, |acc, pred| Expr::Binary {
21722 lhs: alloc::boxed::Box::new(acc),
21723 op: crate::ast::BinOp::And,
21724 rhs: alloc::boxed::Box::new(pred),
21725 }))
21726 } else if kind == JoinKind::Cross {
21727 None
21728 } else {
21729 return Err(self.err(format!(
21730 "expected ON or USING after {:?} JOIN, got {:?}",
21731 kind,
21732 self.peek()
21733 )));
21734 };
21735 joins.push(FromJoin {
21736 kind,
21737 table,
21738 on,
21739 using_cols,
21740 natural: false,
21741 });
21742 }
21743 Ok(joins)
21744 }
21745
21746 /// Optional alias after an expression or table:
21747 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21748 /// accepted (PG-style implicit alias). Returns `None` if the next token
21749 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21750 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21751 if matches!(self.peek(), Token::As) {
21752 self.advance();
21753 // v7.39 (round 340, V56) — after AS the next token MUST be an
21754 // identifier. This used to return None and "let the caller
21755 // surface the error on the next expectation", but when AS is
21756 // the LAST token there is no next expectation: `SELECT 1 AS`
21757 // parsed clean and silently dropped the alias. PG rejects it.
21758 // v7.40.11 — a keyword is a legal alias after AS.
21759 //
21760 // `expect_ident_like` has known the unreserved class since
21761 // v7.17, and this guard never let a keyword token reach it.
21762 // So every one of them was a syntax error in alias position
21763 // while being accepted as a column name in the same build:
21764 //
21765 // CREATE TABLE rk (release int) accepted
21766 // SELECT release FROM rk accepted
21767 // SELECT 1 AS release syntax error
21768 //
21769 // Reported against 7.40.9 for `release` and `savepoint` —
21770 // two statements in a shipped subcommand of the reporter's
21771 // could not be parsed — and it is the whole class, `show`
21772 // and `index` included.
21773 //
21774 // Measured on PG 18.6: after AS, EVERY keyword is a legal
21775 // label, `limit` and `between` included. This accepts the
21776 // ones this parser can name, which is the unreserved class;
21777 // a reserved keyword after AS is still refused here and PG
21778 // takes it.
21779 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21780 return self.expect_ident_like().map(Some);
21781 }
21782 if unreserved_keyword_text(self.peek()).is_some() {
21783 return self.expect_ident_like().map(Some);
21784 }
21785 return Err(self.err(alloc::format!(
21786 "expected an alias after AS, got {:?}",
21787 self.peek()
21788 )));
21789 }
21790 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21791 // grammar reserves a long list of follow-keywords from the
21792 // alias slot. SPG's bareword approximation: skip a small
21793 // set of idents that would otherwise be swallowed as the
21794 // table alias and break trailing clauses like CREATE
21795 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21796 // CONFLICT WHERE shapes.
21797 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21798 if is_alias_stopword(s) {
21799 return Ok(None);
21800 }
21801 return Ok(self.expect_ident_like().ok());
21802 }
21803 // v7.40.11 — and a keyword, WITHOUT `AS`, which PG also takes:
21804 // `SELECT 1 release` answers 1 there.
21805 //
21806 // Not the ones that begin a trailing clause. PG reserves those
21807 // for exactly this reason and so must this: measured on PG 18.6,
21808 // `SELECT 1 limit` is `syntax error at end of input` — it read
21809 // `limit` as the clause, not as a label. Swallowing it here
21810 // would turn `SELECT 1 limit 2` into a two-token nonsense.
21811 if !matches!(self.peek(), Token::Limit | Token::Offset)
21812 && unreserved_keyword_text(self.peek()).is_some()
21813 {
21814 return Ok(self.expect_ident_like().ok());
21815 }
21816 Ok(None)
21817 }
21818
21819 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21820 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21821 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21822 // error beats a stack overflow (an overflow aborts the
21823 // embedding host process).
21824 self.enter_nested()?;
21825 let r = self.parse_expr_inner(min_prec);
21826 self.nest_depth -= 1;
21827 r
21828 }
21829
21830 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21831 /// When the upcoming tokens form one, return the underlying
21832 /// operator token and the position just past the closing paren
21833 /// so the binary loop can dispatch on the plain operator.
21834 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21835 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21836 return None;
21837 }
21838 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21839 return None;
21840 }
21841 let mut i = self.pos + 2;
21842 // Optional schema qualifier (pg_catalog.<op> etc.).
21843 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21844 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21845 {
21846 i += 2;
21847 }
21848 let op_tok = self.tokens.get(i)?.clone();
21849 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21850 return None;
21851 }
21852 Some((i + 2, op_tok))
21853 }
21854
21855 /// PG operator symbols that lower onto function calls in
21856 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21857 /// family → regexp_like, comparison rung), `^@` (starts_with,
21858 /// comparison rung), `^` (power, tighter than `*`), `#`
21859 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21860 /// subset of the OR bits so the subtraction never borrows).
21861 fn try_symbol_operator(
21862 &mut self,
21863 lhs: &Expr,
21864 min_prec: u8,
21865 ) -> Result<Option<Expr>, ParseError> {
21866 enum Sym {
21867 Regex { ci: bool, negated: bool },
21868 Like { ci: bool, negated: bool },
21869 StartsWith,
21870 Power,
21871 Xor,
21872 RangeAdjacent,
21873 }
21874 // v7.39 (IS-precedence knife) — the low-precedence postfix
21875 // predicates ride this existing leaf call (zero new frame slots
21876 // on the nesting chain).
21877 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21878 return Ok(Some(e));
21879 }
21880 let (sym, prec): (Sym, u8) = match self.peek() {
21881 Token::Tilde => (
21882 Sym::Regex {
21883 ci: false,
21884 negated: false,
21885 },
21886 5,
21887 ),
21888 Token::TildeStar => (
21889 Sym::Regex {
21890 ci: true,
21891 negated: false,
21892 },
21893 5,
21894 ),
21895 Token::NotTilde => (
21896 Sym::Regex {
21897 ci: false,
21898 negated: true,
21899 },
21900 5,
21901 ),
21902 Token::NotTildeStar => (
21903 Sym::Regex {
21904 ci: true,
21905 negated: true,
21906 },
21907 5,
21908 ),
21909 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21910 Token::DoubleTilde => (
21911 Sym::Like {
21912 ci: false,
21913 negated: false,
21914 },
21915 5,
21916 ),
21917 Token::DoubleTildeStar => (
21918 Sym::Like {
21919 ci: true,
21920 negated: false,
21921 },
21922 5,
21923 ),
21924 Token::NotDoubleTilde => (
21925 Sym::Like {
21926 ci: false,
21927 negated: true,
21928 },
21929 5,
21930 ),
21931 Token::NotDoubleTildeStar => (
21932 Sym::Like {
21933 ci: true,
21934 negated: true,
21935 },
21936 5,
21937 ),
21938 Token::CaretAt => (Sym::StartsWith, 5),
21939 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21940 // tighter than `* / & |`, which the prec-9 rung preserves —
21941 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21942 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21943 Token::Caret => (Sym::Power, 9),
21944 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21945 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21946 Token::Hash => (Sym::Xor, 6),
21947 Token::Adjacent => (Sym::RangeAdjacent, 5),
21948 _ => return Ok(None),
21949 };
21950 if prec < min_prec {
21951 return Ok(None);
21952 }
21953 self.advance();
21954 let rhs = self.parse_expr(prec + 1)?;
21955 let out = match sym {
21956 Sym::Regex { ci, negated } => {
21957 let mut args = alloc::vec![lhs.clone(), rhs];
21958 if ci {
21959 args.push(Expr::Literal(Literal::String(String::from("i"))));
21960 }
21961 maybe_not(
21962 Expr::FunctionCall {
21963 name: String::from("regexp_like"),
21964 args,
21965 },
21966 negated,
21967 )
21968 }
21969 Sym::Like { ci, negated } => Expr::Like {
21970 expr: alloc::boxed::Box::new(lhs.clone()),
21971 pattern: alloc::boxed::Box::new(rhs),
21972 negated,
21973 case_insensitive: ci,
21974 },
21975 Sym::StartsWith => Expr::FunctionCall {
21976 name: String::from("starts_with"),
21977 args: alloc::vec![lhs.clone(), rhs],
21978 },
21979 Sym::Power => Expr::FunctionCall {
21980 name: String::from("power"),
21981 args: alloc::vec![lhs.clone(), rhs],
21982 },
21983 // `#` bitwise XOR — a real operator now (was desugared to
21984 // `(a|b)-(a&b)`, algebraically identical for integers but
21985 // undefined for bit strings; the direct op handles both).
21986 Sym::Xor => Expr::Binary {
21987 lhs: Box::new(lhs.clone()),
21988 op: BinOp::BitXor,
21989 rhs: Box::new(rhs),
21990 },
21991 // range `-|-` "is adjacent to" — lowered to a catalog function.
21992 Sym::RangeAdjacent => Expr::FunctionCall {
21993 name: String::from("range_adjacent"),
21994 args: alloc::vec![lhs.clone(), rhs],
21995 },
21996 };
21997 Ok(Some(out))
21998 }
21999
22000 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
22001 /// predicates, moved out of the tight postfix-cast loop: PG binds
22002 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
22003 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
22004 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
22005 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
22006 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
22007 /// when nothing at this position belongs to the family. Out-of-line
22008 /// (`inline(never)`): the caller sits on the per-nesting-level frame
22009 /// chain that MAX_NEST_DEPTH is tuned against.
22010 #[inline(never)]
22011 fn parse_postfix_predicate(
22012 &mut self,
22013 lhs: &Expr,
22014 min_prec: u8,
22015 ) -> Result<Option<Expr>, ParseError> {
22016 // Reached through try_symbol_operator (an existing leaf call of
22017 // the binary loop) so NO new stack slots land on the per-nesting
22018 // frame chain; the lhs clones only when a predicate actually
22019 // consumes it.
22020 match self.peek() {
22021 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
22022 // comparison family rung 5 (each +1 from the pre-XOR ladder).
22023 Token::Is if min_prec <= 4 => {}
22024 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
22025 Token::Not
22026 if min_prec <= 5
22027 && matches!(
22028 self.tokens.get(self.pos + 1),
22029 Some(Token::Between | Token::In | Token::Like)
22030 ) => {}
22031 Token::Not | Token::Ident(_)
22032 if min_prec <= 5
22033 && (matches!(self.peek(), Token::Ident(s)
22034 if s.eq_ignore_ascii_case("ilike")
22035 || (self.mysql_dialect
22036 && (s.eq_ignore_ascii_case("regexp")
22037 || s.eq_ignore_ascii_case("rlike")))
22038 || (s.eq_ignore_ascii_case("similar")
22039 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
22040 || (matches!(self.peek(), Token::Not)
22041 && matches!(self.tokens.get(self.pos + 1),
22042 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22043 || (self.mysql_dialect
22044 && (s.eq_ignore_ascii_case("regexp")
22045 || s.eq_ignore_ascii_case("rlike")))
22046 || s.eq_ignore_ascii_case("similar")))) => {}
22047 _ => return Ok(None),
22048 }
22049 let mut expr = lhs.clone();
22050 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
22051 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
22052 if min_prec <= 4 {
22053 if matches!(self.peek(), Token::Is) {
22054 self.advance();
22055 let negated = if matches!(self.peek(), Token::Not) {
22056 self.advance();
22057 true
22058 } else {
22059 false
22060 };
22061 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
22062 // mailrs pg_dump.
22063 if matches!(self.peek(), Token::Distinct) {
22064 self.advance();
22065 if !matches!(self.peek(), Token::From) {
22066 return Err(self.err(format!(
22067 "expected FROM after IS{} DISTINCT, got {:?}",
22068 if negated { " NOT" } else { "" },
22069 self.peek()
22070 )));
22071 }
22072 self.advance();
22073 // Right-hand side: parse at the same precedence
22074 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
22075 // groups as `x IS DISTINCT FROM (a + b)`.
22076 let rhs = self.parse_expr(5)?;
22077 let op = if negated {
22078 BinOp::IsNotDistinctFrom
22079 } else {
22080 BinOp::IsDistinctFrom
22081 };
22082 expr = Expr::Binary {
22083 op,
22084 lhs: Box::new(expr),
22085 rhs: Box::new(rhs),
22086 };
22087 {
22088 return Ok(Some(expr));
22089 }
22090 }
22091 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22092 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22093 // Lowers onto pg_is_json(x, kind); NOT wraps the
22094 // call in a logical negation.
22095 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22096 if s.eq_ignore_ascii_case("json"))
22097 {
22098 self.advance(); // JSON
22099 let kind = match self.peek() {
22100 Token::Ident(s) | Token::QuotedIdent(s)
22101 if matches!(
22102 s.to_ascii_lowercase().as_str(),
22103 "value" | "object" | "array" | "scalar"
22104 ) =>
22105 {
22106 let k = s.to_ascii_lowercase();
22107 self.advance();
22108 k
22109 }
22110 _ => "value".to_string(),
22111 };
22112 let call = Expr::FunctionCall {
22113 name: "pg_is_json".to_string(),
22114 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22115 };
22116 expr = if negated {
22117 Expr::Unary {
22118 op: UnOp::Not,
22119 expr: Box::new(call),
22120 }
22121 } else {
22122 call
22123 };
22124 {
22125 return Ok(Some(expr));
22126 }
22127 }
22128 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22129 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22130 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22131 {
22132 let form_kw = match self.peek() {
22133 Token::Ident(s) | Token::QuotedIdent(s)
22134 if matches!(
22135 s.to_ascii_uppercase().as_str(),
22136 "NFC" | "NFD" | "NFKC" | "NFKD"
22137 ) && matches!(
22138 self.tokens.get(self.pos + 1),
22139 Some(Token::Ident(n) | Token::QuotedIdent(n))
22140 if n.eq_ignore_ascii_case("normalized")
22141 ) =>
22142 {
22143 Some(s.to_ascii_uppercase())
22144 }
22145 _ => None,
22146 };
22147 let bare_normalized = form_kw.is_none()
22148 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22149 if s.eq_ignore_ascii_case("normalized"));
22150 if form_kw.is_some() || bare_normalized {
22151 if form_kw.is_some() {
22152 self.advance(); // form keyword
22153 }
22154 self.advance(); // NORMALIZED
22155 let mut args = alloc::vec![expr];
22156 if let Some(f) = form_kw {
22157 args.push(Expr::Literal(Literal::String(f)));
22158 }
22159 let call = Expr::FunctionCall {
22160 name: "is_normalized".to_string(),
22161 args,
22162 };
22163 expr = if negated {
22164 Expr::Unary {
22165 op: UnOp::Not,
22166 expr: Box::new(call),
22167 }
22168 } else {
22169 call
22170 };
22171 {
22172 return Ok(Some(expr));
22173 }
22174 }
22175 }
22176 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22177 // three-valued boolean tests. IS TRUE/FALSE never
22178 // return NULL, so they lower to CASE forms whose
22179 // ELSE catches the NULL branch; IS UNKNOWN on a
22180 // boolean is exactly IS NULL.
22181 if matches!(self.peek(), Token::True | Token::False)
22182 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22183 {
22184 let tok = self.advance();
22185 let test = match tok {
22186 Token::True => Some(true),
22187 Token::False => Some(false),
22188 _ => None, // UNKNOWN
22189 };
22190 // v7.39 (round 328, V45) — kept as what the user
22191 // wrote. These used to be lowered here into `CASE` /
22192 // `IS NULL`; the semantics were right but the AST no
22193 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22194 // was echoed back as
22195 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22196 expr = Expr::BoolTest {
22197 expr: Box::new(expr),
22198 value: test,
22199 negated,
22200 };
22201 {
22202 return Ok(Some(expr));
22203 }
22204 }
22205 if !matches!(self.peek(), Token::Null) {
22206 return Err(self.err(format!(
22207 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22208 if negated { " NOT" } else { "" },
22209 self.peek()
22210 )));
22211 }
22212 self.advance();
22213 expr = Expr::IsNull {
22214 expr: Box::new(expr),
22215 negated,
22216 };
22217 {
22218 return Ok(Some(expr));
22219 }
22220 }
22221 }
22222 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22223 if min_prec <= 5 {
22224 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22225 // Look one token ahead so a stray `NOT` not followed by any of
22226 // these flows through to the early return below untouched.
22227 let negated = if matches!(self.peek(), Token::Not) {
22228 let next = self.tokens.get(self.pos + 1);
22229 matches!(next, Some(Token::Between | Token::In | Token::Like))
22230 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22231 || (self.mysql_dialect
22232 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22233 || s.eq_ignore_ascii_case("similar"))
22234 } else {
22235 false
22236 };
22237 if negated {
22238 self.advance();
22239 }
22240 if matches!(self.peek(), Token::Between) {
22241 expr = self.parse_between_tail(expr, negated)?;
22242 {
22243 return Ok(Some(expr));
22244 }
22245 }
22246 if matches!(self.peek(), Token::In) {
22247 if self.suppress_in_tail && !negated {
22248 // POSITION(sub IN str) — IN belongs to the
22249 // enclosing function syntax; stop here.
22250 {
22251 return Ok(None);
22252 }
22253 }
22254 expr = self.parse_in_tail(expr, negated)?;
22255 {
22256 return Ok(Some(expr));
22257 }
22258 }
22259 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22260 // lowers onto the internal __similar_to(expr, pat[, esc]) call
22261 // (the SQL→regex transform runs inside, in the backtracking-
22262 // friendly shape SPG's matcher needs).
22263 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22264 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22265 {
22266 self.advance(); // SIMILAR
22267 self.advance(); // TO
22268 let pattern = self.parse_expr(6)?;
22269 let mut args = alloc::vec![expr, pattern];
22270 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22271 self.advance();
22272 args.push(self.parse_expr(6)?);
22273 }
22274 let call = Expr::FunctionCall {
22275 name: "__similar_to".to_string(),
22276 args,
22277 };
22278 expr = maybe_not(call, negated);
22279 {
22280 return Ok(Some(expr));
22281 }
22282 }
22283 if matches!(self.peek(), Token::Like) {
22284 self.advance();
22285 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22286 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22287 expr = q;
22288 {
22289 return Ok(Some(expr));
22290 }
22291 }
22292 // Pattern at the same precedence as other comparison RHSes —
22293 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22294 let mut pattern = self.parse_expr(6)?;
22295 // `ESCAPE 'c'` — rewrite a literal pattern to the
22296 // default backslash escape at parse time. Custom
22297 // escapes on non-literal patterns would need
22298 // matcher support; error honestly.
22299 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22300 self.advance();
22301 let esc = self.parse_expr(6)?;
22302 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22303 }
22304 expr = Expr::Like {
22305 expr: Box::new(expr),
22306 pattern: Box::new(pattern),
22307 negated,
22308 case_insensitive: false,
22309 };
22310 {
22311 return Ok(Some(expr));
22312 }
22313 }
22314 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22315 // keyword reaches us as a plain identifier.
22316 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22317 self.advance();
22318 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22319 expr = q;
22320 {
22321 return Ok(Some(expr));
22322 }
22323 }
22324 let pattern = self.parse_expr(6)?;
22325 expr = Expr::Like {
22326 expr: Box::new(expr),
22327 pattern: Box::new(pattern),
22328 negated,
22329 case_insensitive: true,
22330 };
22331 {
22332 return Ok(Some(expr));
22333 }
22334 }
22335 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22336 // operator (RLIKE is the alias). It is a keyword, not `~`, and
22337 // matches case-insensitively under the default collation, so it
22338 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22339 // `~*` operator uses, wrapped in NOT when negated.
22340 if self.mysql_dialect
22341 && matches!(self.peek(), Token::Ident(s)
22342 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22343 {
22344 self.advance();
22345 let pattern = self.parse_expr(6)?;
22346 let call = Expr::FunctionCall {
22347 name: String::from("regexp_like"),
22348 args: alloc::vec![
22349 expr,
22350 pattern,
22351 Expr::Literal(Literal::String(String::from("i"))),
22352 ],
22353 };
22354 return Ok(Some(maybe_not(call, negated)));
22355 }
22356 }
22357 let _ = expr;
22358 Ok(None)
22359 }
22360
22361 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22362 let mut lhs = self.parse_unary()?;
22363 let mut chain_len = 0usize;
22364 loop {
22365 // OPERATOR([schema.]op) reduces to its underlying
22366 // operator token before the normal dispatch.
22367 let explicit = self.peek_explicit_operator();
22368 let dispatch = match &explicit {
22369 Some((_, tok)) => self.binop_here(tok),
22370 None => self.binop_here(self.peek()),
22371 };
22372 let Some((op, prec)) = dispatch else {
22373 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22374 // of the symbol family. `binop_here` answers None for them
22375 // because they lower onto function calls rather than a
22376 // BinOp, and the fallback below reads `self.peek()` — the
22377 // word OPERATOR, not the operator. `pg_dump` writes every
22378 // catalog predicate this way, so its first query failed
22379 // and no dump ran:
22380 //
22381 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22382 //
22383 // Collapsing the wrapper to the operator it names puts the
22384 // token where the fallback already looks.
22385 if let Some((next, op_tok)) = explicit {
22386 self.tokens.splice(self.pos..next, [op_tok]);
22387 }
22388 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22389 lhs = e;
22390 chain_len += 1;
22391 if chain_len > MAX_BINARY_CHAIN {
22392 return Err(self.err(alloc::format!(
22393 "more than {MAX_BINARY_CHAIN} chained binary operators"
22394 )));
22395 }
22396 continue;
22397 }
22398 break;
22399 };
22400 if prec < min_prec {
22401 break;
22402 }
22403 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22404 // iteratively but evaluates and drops recursively;
22405 // depth beyond the budget overflows worker stacks.
22406 chain_len += 1;
22407 if chain_len > MAX_BINARY_CHAIN {
22408 return Err(self.err(alloc::format!(
22409 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22410 )));
22411 }
22412 match explicit {
22413 Some((end_pos, _)) => self.pos = end_pos,
22414 None => {
22415 self.advance();
22416 }
22417 }
22418 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22419 // ANY is a bare ident; ALL is a reserved Token. Both
22420 // require an immediate `(` to disambiguate from
22421 // identifier columns named `any` / `all`.
22422 let any_kind = match self.peek() {
22423 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22424 Some(false)
22425 }
22426 Token::Ident(s) | Token::QuotedIdent(s)
22427 if (s.eq_ignore_ascii_case("any")
22428 || s.eq_ignore_ascii_case("some")
22429 || s.eq_ignore_ascii_case("all"))
22430 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22431 {
22432 Some(!s.eq_ignore_ascii_case("all"))
22433 }
22434 _ => None,
22435 };
22436 if let Some(is_any) = any_kind {
22437 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22438 continue;
22439 }
22440 let rhs = self.parse_expr(prec + 1)?;
22441 lhs = Expr::Binary {
22442 lhs: Box::new(lhs),
22443 op,
22444 rhs: Box::new(rhs),
22445 };
22446 }
22447 Ok(lhs)
22448 }
22449
22450 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22451 /// and the array form.
22452 ///
22453 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22454 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22455 /// this block's `Expr` temporaries and four `format!` sites slots in
22456 /// that frame on every level of `((((1))))`, which never reaches it.
22457 #[inline(never)]
22458 fn parse_any_all_rhs(
22459 &mut self,
22460 lhs: Expr,
22461 op: BinOp,
22462 is_any: bool,
22463 ) -> Result<Expr, ParseError> {
22464 self.advance(); // ident
22465 self.advance(); // (
22466 // `x op ANY (SELECT …)` — the quantified-subquery
22467 // form. `= ANY` is exactly IN; the other operators
22468 // lower onto EXISTS over the subquery as a derived
22469 // table, comparing against its single projection
22470 // aliased __v (x's columns resolve correlated).
22471 // ALL is the negated-EXISTS complement; a NULL
22472 // element makes PG return NULL where this lowering
22473 // returns true — the NOT NULL column case (the
22474 // practical one) is exact.
22475 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22476 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22477 // legal PG too (round-151 sibling). Out-of-line
22478 // (#[inline(never)] helper) — this sits on
22479 // parse_expr's recursive frame and the two-armed
22480 // SELECT temporary blew the nesting-budget stack.
22481 let mut sub = self.parse_any_all_select_body()?;
22482 if !matches!(self.peek(), Token::RParen) {
22483 return Err(self.err(alloc::format!(
22484 "expected ')' after ANY/ALL subquery, got {:?}",
22485 self.peek()
22486 )));
22487 }
22488 self.advance();
22489 if sub.items.len() != 1 {
22490 return Err(self.err(alloc::format!(
22491 "ANY/ALL subquery must return one column, got {}",
22492 sub.items.len()
22493 )));
22494 }
22495 if is_any && matches!(op, BinOp::Eq) {
22496 return Ok(Expr::InSubquery {
22497 expr: Box::new(lhs),
22498 subquery: Box::new(sub),
22499 negated: false,
22500 });
22501 }
22502 // The engine's subquery resolvers materialise
22503 // the single-column result into an ARRAY the
22504 // existing AnyAll three-valued eval consumes.
22505 return Ok(Expr::AnyAll {
22506 expr: Box::new(lhs),
22507 op,
22508 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22509 is_any,
22510 });
22511 }
22512 let arr = self.parse_expr(0)?;
22513 if !matches!(self.peek(), Token::RParen) {
22514 return Err(self.err(alloc::format!(
22515 "expected ')' after ANY/ALL argument, got {:?}",
22516 self.peek()
22517 )));
22518 }
22519 self.advance();
22520 Ok(Expr::AnyAll {
22521 expr: Box::new(lhs),
22522 op,
22523 array: Box::new(arr),
22524 is_any,
22525 })
22526 }
22527
22528 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22529 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22530 #[inline(never)]
22531 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22532 self.advance();
22533 let e = self.parse_expr(9)?;
22534 Ok(build_center_call(e))
22535 }
22536
22537 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22538 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22539 /// unary minus.
22540 ///
22541 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22542 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22543 /// the Expr-sized local stays out of that frame.
22544 #[inline(never)]
22545 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22546 self.advance();
22547 let e = self.parse_expr(9)?;
22548 Ok(Expr::FunctionCall {
22549 name: alloc::string::String::from(name),
22550 args: alloc::vec![e],
22551 })
22552 }
22553
22554 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22555 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22556 #[inline(never)]
22557 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22558 self.advance();
22559 let e = self.parse_expr(9)?;
22560 Ok(Expr::FunctionCall {
22561 name: alloc::string::String::from(if vertical {
22562 "isvertical"
22563 } else {
22564 "ishorizontal"
22565 }),
22566 args: alloc::vec![e],
22567 })
22568 }
22569
22570 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22571 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22572 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22573 #[inline(never)]
22574 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22575 self.advance();
22576 let e = self.parse_expr(9)?;
22577 Ok(Expr::Cast {
22578 expr: Box::new(e),
22579 target: CastTarget::Named("binary".to_string()),
22580 })
22581 }
22582
22583 /// The prefix operators that share one shape: take the token, parse
22584 /// an operand at `prec`, wrap it.
22585 ///
22586 /// `#[inline(never)]`, and one function instead of five arms, for the
22587 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22588 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22589 /// debug build gives EVERY arm's locals a slot in the frame, whichever
22590 /// arm runs. `((((1))))` reaches none of these arms and was carrying
22591 /// five `Expr`-sized locals per level for them anyway.
22592 #[inline(never)]
22593 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22594 self.advance();
22595 let e = self.parse_expr(prec)?;
22596 Ok(Expr::Unary {
22597 op,
22598 expr: Box::new(e),
22599 })
22600 }
22601
22602 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22603 /// and separate from it because of the literal folding below and the
22604 /// `format!` temporaries that folding needs.
22605 #[inline(never)]
22606 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22607 self.advance();
22608 // v7.39 (round 549) — fold the sign into an integer literal that
22609 // only fits once it is negative.
22610 //
22611 // `9223372036854775808` is one past i64::MAX, so the lexer hands
22612 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22613 // folds the sign first, so `-9223372036854775808` is a bigint
22614 // there — and `-9223372036854775808 - 1` raises "bigint out of
22615 // range" where SPG quietly answered -9223372036854775809, a value
22616 // no bigint can hold. The arithmetic itself was already checked;
22617 // only the literal's type was wrong.
22618 if let Token::Numeric(lit) = self.peek()
22619 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22620 {
22621 self.advance();
22622 return Ok(Expr::Literal(Literal::Integer(folded)));
22623 }
22624 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22625 // `<->` slotted into 5 and arithmetic shifted up).
22626 let e = self.parse_expr(9)?;
22627 Ok(Expr::Unary {
22628 op: UnOp::Neg,
22629 expr: Box::new(e),
22630 })
22631 }
22632
22633 /// tsquery `!!` prefix negation, lowered to the catalog function.
22634 /// Binds like unary minus. Out-of-line for the frame reason on
22635 /// `parse_unary_op`.
22636 #[inline(never)]
22637 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22638 self.advance();
22639 let e = self.parse_expr(9)?;
22640 Ok(Expr::FunctionCall {
22641 name: String::from("tsquery_not"),
22642 args: alloc::vec![e],
22643 })
22644 }
22645
22646 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22647 match self.peek() {
22648 // NOT binds tighter than AND / XOR / OR but looser than
22649 // comparisons — its operand takes everything ≥ the comparison
22650 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22651 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22652 // was rung 3, behaviour-identical when 3 was unused; AND now
22653 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22654 Token::Not => self.parse_unary_op(UnOp::Not, 4),
22655 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22656 // The body is out-of-line: `parse_unary` is one of the three
22657 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22658 // inline arm here overflowed the native stack in
22659 // `nesting_budget_errors_cleanly` — the guard test caught it,
22660 // exactly as the eval-side cliff did in rounds 346 and 351.
22661 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22662 self.parse_binary_prefix()
22663 }
22664 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22665 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22666 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22667 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22668 Token::Minus => self.parse_prefix_minus(),
22669 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22670 // worked only because the lexer reads it as one signed literal;
22671 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22672 // PG18 and MariaDB take all of them. Binds like unary minus.
22673 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22674 // Bitwise NOT binds like unary minus.
22675 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22676 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22677 // "center of" operator; desugars to center(x). The whole arm
22678 // is out-of-line: parse_unary sits on the per-nesting-level
22679 // frame chain that MAX_NEST_DEPTH is tuned against, so no
22680 // Expr-sized local may live in this frame.
22681 Token::TsMatch => self.parse_prefix_center(),
22682 // v7.39 (round 508) — the prefix operators that are named
22683 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22684 // is length. Out-of-line for the same nesting-frame reason as
22685 // parse_prefix_center — parse_unary sits on the recursive cycle
22686 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22687 // live in this frame.
22688 Token::At => self.parse_prefix_call("abs"),
22689 Token::Hash => self.parse_prefix_call("npoints"),
22690 Token::AtMinusAt => self.parse_prefix_call("length"),
22691 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22692 // "is horizontal" (lseg / line); desugars to the existing
22693 // isvertical()/ishorizontal() functions. Out-of-line for the
22694 // same nesting-frame reason as parse_prefix_center.
22695 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22696 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22697 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22698 _ => self.parse_atom(),
22699 }
22700 }
22701
22702 /// Parse a parenthesised scalar subquery body after the caller has consumed
22703 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22704 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22705 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22706 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22707 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22708 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22709 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22710 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22711 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22712 /// tips the deep-nesting test into a stack overflow).
22713 #[inline(never)]
22714 fn array_subquery_ahead(&self) -> bool {
22715 if !matches!(self.peek(), Token::LParen) {
22716 return false;
22717 }
22718 matches!(
22719 self.tokens.get(self.pos + 1),
22720 Some(Token::Select | Token::Values)
22721 ) || matches!(
22722 self.tokens.get(self.pos + 1),
22723 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22724 )
22725 }
22726
22727 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22728 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22729 /// locals stay off parse_atom's recursive frame (round 105).
22730 #[inline(never)]
22731 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22732 self.advance(); // consume `[`
22733 let mut items: Vec<Expr> = Vec::new();
22734 if !matches!(self.peek(), Token::RBracket) {
22735 loop {
22736 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22737 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22738 if matches!(self.peek(), Token::LBracket) {
22739 items.push(self.parse_array_bracket_body()?);
22740 } else {
22741 items.push(self.parse_expr(0)?);
22742 }
22743 match self.peek() {
22744 Token::Comma => {
22745 self.advance();
22746 }
22747 Token::RBracket => break,
22748 other => {
22749 return Err(self.err(alloc::format!(
22750 "expected ',' or ']' in ARRAY literal, got {other:?}"
22751 )));
22752 }
22753 }
22754 }
22755 }
22756 self.advance(); // consume `]`
22757 Ok(Expr::Array(items))
22758 }
22759
22760 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22761 /// is already consumed; the current token is `(`. Desugars to a scalar
22762 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22763 /// the subquery's single-column rows in order — reusing the existing
22764 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22765 /// keeps the large `Statement` local off parse_atom's recursive frame.
22766 #[inline(never)]
22767 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22768 self.advance(); // consume `(`
22769 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22770 if w.eq_ignore_ascii_case("with"));
22771 let sub = if is_with {
22772 self.advance(); // WITH
22773 self.parse_with_cte_then_select()?
22774 } else {
22775 self.parse_select_stmt()?
22776 };
22777 if !matches!(self.peek(), Token::RParen) {
22778 return Err(self.err(alloc::format!(
22779 "expected ')' to close ARRAY(subquery), got {:?}",
22780 self.peek()
22781 )));
22782 }
22783 self.advance(); // consume `)`
22784 // Reuse the parser to build the array_agg wrapper from the subquery's
22785 // canonical text — avoids hand-constructing the derived-table AST.
22786 let wrapper = alloc::format!(
22787 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22788 );
22789 let stmt = parse_statement(&wrapper)
22790 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22791 let Statement::Select(sel) = stmt else {
22792 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22793 };
22794 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22795 }
22796
22797 #[inline(never)]
22798 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22799 let inner = if is_with {
22800 self.advance(); // WITH
22801 self.parse_with_cte_then_select()?
22802 } else {
22803 self.parse_select_stmt()?
22804 };
22805 match self.advance() {
22806 Token::RParen => {
22807 let Statement::Select(s) = inner else {
22808 return Err(ParseError {
22809 message: "scalar subquery body must be a SELECT".into(),
22810 token_pos: self.consumed_pos(),
22811 });
22812 };
22813 Ok(Expr::ScalarSubquery(Box::new(s)))
22814 }
22815 other => Err(ParseError {
22816 message: format!("expected ')' after scalar subquery, got {other:?}"),
22817 token_pos: self.consumed_pos(),
22818 }),
22819 }
22820 }
22821
22822 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22823 /// literals. The lexer splits them into an ident + string; recombine
22824 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22825 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22826 /// frame for the `body` / `bits` strings and their char loops (the
22827 /// round-367 frame cliff, M20).
22828 #[inline(never)]
22829 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22830 let is_hex = match self.peek() {
22831 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22832 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22833 _ => return None,
22834 };
22835 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22836 return None;
22837 }
22838 // v7.39.3 — where the LITERAL starts, because the errors below
22839 // are about the literal and both engines point at it. `err`
22840 // reports the CURRENT token, which by then is the one after the
22841 // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22842 // `near '…'` snippet — which runs from the reported position to
22843 // the end — came out empty where MySQL 9.7.2 says `near
22844 // 'x'123''`.
22845 let lit_pos = self.pos;
22846 self.advance();
22847 let Token::String(body) = self.advance() else {
22848 unreachable!("guarded above");
22849 };
22850 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22851 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22852 // (hex pairs, even count required — MariaDB errors on an odd
22853 // count); `b'1010'` packs its bits big-endian, left-padded to a
22854 // byte. Lower both onto the bytea cast.
22855 if self.mysql_dialect {
22856 if is_hex {
22857 if body.len() % 2 == 1 {
22858 return Some(Err(self.err_at(
22859 lit_pos,
22860 alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22861 )));
22862 }
22863 for c in body.chars() {
22864 if !c.is_ascii_hexdigit() {
22865 return Some(Err(self.err_at(
22866 lit_pos,
22867 alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22868 )));
22869 }
22870 }
22871 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22872 }
22873 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22874 return Some(Err(self.err_at(
22875 lit_pos,
22876 alloc::format!("invalid binary digit {bad:?} in b'…'"),
22877 )));
22878 }
22879 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22880 }
22881 let bits = if is_hex {
22882 let mut out = String::with_capacity(body.len() * 4);
22883 for c in body.chars() {
22884 let Some(d) = c.to_digit(16) else {
22885 // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22886 // own quoting: `"g" is not a valid hexadecimal
22887 // digit` (measured, with the caret on the literal).
22888 return Some(Err(self.err_at(
22889 lit_pos,
22890 alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22891 )));
22892 };
22893 out.push_str(&alloc::format!("{d:04b}"));
22894 }
22895 out
22896 } else {
22897 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22898 return Some(Err(self.err_at(
22899 lit_pos,
22900 alloc::format!("\"{bad}\" is not a valid binary digit"),
22901 )));
22902 }
22903 body
22904 };
22905 // Route through the postfix-cast loop so a chained cast like
22906 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22907 // of erroring at the `::`.
22908 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22909 // literal keeps its exact length, while an explicit `::bit` cast is
22910 // bit(1) with pad/truncate semantics (PG).
22911 Some(self.finish_postfix_casts(Expr::Cast {
22912 expr: Box::new(Expr::Literal(Literal::String(bits))),
22913 target: CastTarget::Named("__bit_literal".to_string()),
22914 }))
22915 }
22916
22917 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22918 if let Some(res) = self.try_parse_bit_string_literal() {
22919 return res;
22920 }
22921 // v7.40.11 — `<alias>.*` in an EXPRESSION is the whole row, the
22922 // same thing the bare alias is.
22923 //
22924 // Reported against 7.40.9: `to_jsonb(t.*)`, `row_to_json(t.*)`,
22925 // `pg_column_size(t.*)` and `count(t.*)` were all
22926 // `syntax error at or near "*"`, while `to_jsonb(t)` — the same
22927 // value, differently spelled — worked. Measured on PG 18.6, the
22928 // two spellings answer byte-identically:
22929 //
22930 // to_jsonb(t.*) {"a": 1, "b": "x"}
22931 // to_jsonb(t) {"a": 1, "b": "x"}
22932 //
22933 // Here rather than in the argument list: nothing else in an
22934 // expression position is `ident . *`, and a select item's own
22935 // `t.*` is recognised before `parse_expr` is ever called, so
22936 // `SELECT t.* FROM t` keeps expanding to the column list.
22937 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone()
22938 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22939 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
22940 {
22941 self.advance();
22942 self.advance();
22943 self.advance();
22944 return Ok(Expr::Column(ColumnName {
22945 qualifier: None,
22946 name: q,
22947 }));
22948 }
22949 let tok_pos = self.pos;
22950 match self.advance() {
22951 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22952 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22953 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22954 // carrying the source mantissa + scale so no precision is lost. A
22955 // literal too wide for i128 falls back to double precision.
22956 // Out-of-line (#[inline(never)]) — this arm sits on the
22957 // parse_expr recursion chain; its expansion locals must not
22958 // widen the recursive frame (debug frame-cliff discipline).
22959 Token::Numeric(s) => match numeric_token_to_literal(s) {
22960 Ok(lit) => Ok(Expr::Literal(lit)),
22961 Err(msg) => Err(self.err(msg)),
22962 },
22963 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22964 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22965 // (the lexer only emits this token in the MySQL dialect). Lower
22966 // onto the existing bytea cast; out-of-line to keep this arm off
22967 // the parse recursion frame.
22968 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22969 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22970 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22971 Token::Null => Ok(Expr::Literal(Literal::Null)),
22972 // v6.1.1 — `$N` placeholder. The actual Value lookup
22973 // happens in the engine eval path against the prepared-
22974 // statement bind buffer.
22975 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22976 Token::LParen => {
22977 // v4.10: `(SELECT ...)` in expression position is a
22978 // scalar subquery; otherwise it's a parenthesised
22979 // expression. Peek for SELECT keyword to dispatch.
22980 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22981 // lexes as Ident("with") (not a reserved token). The subquery body
22982 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22983 // so its large `Statement` local stays out of parse_atom's stack
22984 // frame — parse_atom is on the recursive `((…))` cycle and the
22985 // nesting budget is tuned to its frame size).
22986 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22987 if s.eq_ignore_ascii_case("with"));
22988 if matches!(self.peek(), Token::Select) || is_with {
22989 self.parse_paren_scalar_subquery(is_with)
22990 } else {
22991 let e = self.parse_expr(0)?;
22992 // `(a, b, …)` — a row constructor. Valid only
22993 // in front of a comparison operator or [NOT]
22994 // IN; both expand at parse time (lexicographic
22995 // comparison / OR'd row equalities).
22996 if matches!(self.peek(), Token::Comma) {
22997 let mut row = alloc::vec![e];
22998 while matches!(self.peek(), Token::Comma) {
22999 self.advance();
23000 row.push(self.parse_expr(0)?);
23001 }
23002 if !matches!(self.peek(), Token::RParen) {
23003 return Err(self.err(alloc::format!(
23004 "expected ')' after row constructor, got {:?}",
23005 self.peek()
23006 )));
23007 }
23008 self.advance();
23009 // A bare `(a, b, …)` row constructor can carry postfix
23010 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
23011 // early return here skips parse_atom's tail postfix
23012 // pass, so fold casts in explicitly. For the
23013 // comparison / predicate forms nothing postfix follows,
23014 // so this is a no-op.
23015 return self
23016 .parse_row_comparison_tail(row)
23017 .and_then(|e| self.finish_postfix_casts(e));
23018 }
23019 match self.advance() {
23020 Token::RParen => Ok(e),
23021 other => Err(ParseError {
23022 message: format!("expected ')', got {other:?}"),
23023 token_pos: self.consumed_pos(),
23024 }),
23025 }
23026 }
23027 }
23028 Token::LBracket => self.parse_vector_literal_body(),
23029 Token::Extract => self.parse_extract_atom(),
23030 Token::Interval => self.parse_interval_atom(),
23031 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
23032 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
23033 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
23034 // expression position calling the PG `left(string, n)` /
23035 // `right(string, n)` function; rebuild the AST as a regular
23036 // function call so the engine's apply_function dispatch picks
23037 // it up. Delegated to a #[inline(never)] helper so its locals
23038 // don't bloat this recursive `parse_atom` frame (the nesting
23039 // budget in `enter_nested` is tuned to parse_atom's size).
23040 Token::Left if matches!(self.peek(), Token::LParen) => {
23041 self.parse_lr_string_function_call("left")
23042 }
23043 Token::Right if matches!(self.peek(), Token::LParen) => {
23044 self.parse_lr_string_function_call("right")
23045 }
23046 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
23047 // token; we match on the bare ident. NOT is a token
23048 // (consumed in the comparison rung), but `EXISTS (...)`
23049 // at the top of an expression starts here.
23050 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
23051 self.parse_exists_atom(false)
23052 }
23053 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
23054 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
23055 // CASE is a bare ident; we dispatch on lowercase match.
23056 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
23057 self.parse_case_atom()
23058 }
23059 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
23060 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
23061 // '…'`. Lower onto the ::cast node so the existing
23062 // runtime text→date/timestamp paths do the parsing. The
23063 // string must follow immediately, else the ident stays a
23064 // plain column reference.
23065 Token::Ident(s)
23066 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
23067 && matches!(self.peek(), Token::String(_)) =>
23068 {
23069 let target =
23070 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
23071 let Token::String(lit) = self.advance() else {
23072 unreachable!("peek guaranteed a string token");
23073 };
23074 Ok(Expr::Cast {
23075 expr: Box::new(Expr::Literal(Literal::String(lit))),
23076 target,
23077 })
23078 }
23079 // v7.39 (round 221) — the SQL-standard long spellings:
23080 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
23081 // TIME ZONE '…'`. Consume the modifier and lower to the same
23082 // typed-literal cast (`timetz` / `timestamptz` for WITH).
23083 Token::Ident(s)
23084 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
23085 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
23086 || w.eq_ignore_ascii_case("without"))
23087 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
23088 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
23089 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
23090 {
23091 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
23092 self.advance(); // WITH / WITHOUT
23093 self.advance(); // TIME
23094 self.advance(); // ZONE
23095 let Token::String(lit) = self.advance() else {
23096 unreachable!("guard checked a string token");
23097 };
23098 let base = s.to_ascii_lowercase();
23099 let target = match (base.as_str(), with_tz) {
23100 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
23101 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
23102 (_, true) => CastTarget::Timestamptz,
23103 (_, false) => CastTarget::Timestamp,
23104 };
23105 Ok(Expr::Cast {
23106 expr: Box::new(Expr::Literal(Literal::String(lit))),
23107 target,
23108 })
23109 }
23110 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23111 // gathers the subquery's single-column rows (in its row order)
23112 // into an array. Desugared to `array_agg` over the subquery as a
23113 // derived table; out-of-line to keep parse_atom's frame small (it
23114 // sits on the recursive nesting-budget cycle).
23115 Token::Ident(s) | Token::QuotedIdent(s)
23116 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23117 {
23118 self.parse_array_subquery()
23119 }
23120 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23121 // is not a reserved token; we match by case-insensitive
23122 // ident. The opening `[` must follow immediately. v7.39 (read01
23123 // round 105) — the body moved out-of-line so its `Vec`/loop locals
23124 // leave parse_atom's frame (which sits on the nesting-budget cycle).
23125 Token::Ident(s) | Token::QuotedIdent(s)
23126 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23127 {
23128 self.parse_array_literal_body()
23129 }
23130 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23131 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23132 // We special-case before the generic ident dispatch so
23133 // the AGAINST clause never reaches the function-call
23134 // loop (which would mis-read `(cols) AGAINST` as a
23135 // call with no trailing modifier). The shape is
23136 // rewritten to a Boolean OR over per-column
23137 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23138 // term)` so the existing FTS evaluator handles
23139 // semantics — the fulltext-GIN built at CREATE TABLE
23140 // time is currently a "real index that survives dump
23141 // round-trip"; the planner hook that actually uses
23142 // it for posting-list intersection lands in a later
23143 // sub-phase (Phase 2.2b) without touching this surface.
23144 Token::Ident(s) | Token::QuotedIdent(s)
23145 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23146 {
23147 self.parse_match_against_atom()
23148 }
23149 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23150 // v7.37.43-T4 — PG-unreserved keywords are legal column /
23151 // alias names in expression context too. `release` appears
23152 // in sentori `0003_partition_events.sql` as both a column
23153 // reference (SELECT … release …) and an INSERT column list
23154 // entry. Mirrors `expect_ident_like`'s expansion of the
23155 // identifier set.
23156 other if unreserved_keyword_text(&other).is_some() => {
23157 let s = unreserved_keyword_text(&other).unwrap();
23158 self.finish_ident_atom(s)
23159 }
23160 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23161 // only inside `SET` before, so `SELECT @@autocommit` — which
23162 // every MySQL connector asks at handshake — was a parse error.
23163 // MariaDB accepts the bare, `@@session.` and `@@global.`
23164 // spellings alike and answers from the session's own value.
23165 Token::SessionVar(v) => {
23166 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23167 // has nothing to do with a `@@` engine setting: its own
23168 // per-session namespace, and an unset one reads NULL instead
23169 // of raising. Stripping every `@` (as this did) made `@x` and
23170 // `@@x` the same node, so `SELECT @x` answered "Unknown
23171 // system variable".
23172 Ok(variable_ref_atom(&v))
23173 }
23174 other => Err(ParseError {
23175 message: format!("unexpected token {other:?} in expression"),
23176 token_pos: tok_pos,
23177 }),
23178 }
23179 // After parsing the atom, fold any postfix `::vector` casts.
23180 .and_then(|atom| self.finish_postfix_casts(atom))
23181 }
23182
23183 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23184 /// Both bind tighter than any binary op.
23185 /// Shared cast-target parser for postfix `::TYPE` and the
23186 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23187 /// If the next tokens are `( N )`, consume them and return the canonical
23188 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23189 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23190 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23191 if !matches!(self.peek(), Token::LParen) {
23192 return None;
23193 }
23194 self.advance(); // (
23195 let n = match self.advance() {
23196 Token::Integer(n) => n,
23197 _ => return Some(base.to_string()), // malformed → drop precision
23198 };
23199 if matches!(self.peek(), Token::RParen) {
23200 self.advance();
23201 }
23202 Some(alloc::format!("{base}({n})"))
23203 }
23204
23205 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23206 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23207 // schema-qualifies every cast target, and `pg_catalog.X` names
23208 // exactly the builtin type X. Consume the qualifier and let
23209 // the ordinary target parse decide.
23210 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23211 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23212 {
23213 self.advance();
23214 self.advance();
23215 }
23216 let target = match self.advance() {
23217 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23218 "int" | "integer" | "int4" => {
23219 if matches!(self.peek(), Token::LBracket)
23220 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23221 {
23222 self.advance();
23223 self.advance();
23224 CastTarget::IntArray
23225 } else {
23226 CastTarget::Int
23227 }
23228 }
23229 "bigint" | "int8" => {
23230 if matches!(self.peek(), Token::LBracket)
23231 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23232 {
23233 self.advance();
23234 self.advance();
23235 CastTarget::BigIntArray
23236 } else {
23237 CastTarget::BigInt
23238 }
23239 }
23240 "float" | "double" => CastTarget::Float,
23241 "text" => {
23242 // v7.10.11 — `::TEXT[]` widens to TextArray.
23243 if matches!(self.peek(), Token::LBracket)
23244 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23245 {
23246 self.advance();
23247 self.advance();
23248 CastTarget::TextArray
23249 } else {
23250 CastTarget::Text
23251 }
23252 }
23253 "bool" | "boolean" => CastTarget::Bool,
23254 "vector" => CastTarget::Vector,
23255 "date" => CastTarget::Date,
23256 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23257 // seconds precision through the Named path (the engine rounds
23258 // the sub-second field); bare `::timestamp` keeps the fast arm.
23259 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23260 Some(named) => CastTarget::Named(named),
23261 None => CastTarget::Timestamp,
23262 },
23263 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23264 Some(named) => CastTarget::Named(named),
23265 None => CastTarget::Timestamptz,
23266 },
23267 "interval" => CastTarget::Interval,
23268 "json" => CastTarget::Json,
23269 "jsonb" => CastTarget::Jsonb,
23270 // v7.39 (round 694) — these have dedicated CastTarget
23271 // variants, so they never reached the postfix `[]` handling
23272 // further down and `::regtype[]` was a SYNTAX error at the
23273 // `]`. PG has an array type for every scalar; take the
23274 // suffix here and hand the canonical `<ty>_array` name to
23275 // the engine, the same shape every other array cast uses.
23276 "regtype" if self.peek_postfix_array_brackets() => {
23277 self.advance();
23278 self.advance();
23279 CastTarget::Named(alloc::string::String::from("regtype_array"))
23280 }
23281 "regclass" if self.peek_postfix_array_brackets() => {
23282 self.advance();
23283 self.advance();
23284 CastTarget::Named(alloc::string::String::from("regclass_array"))
23285 }
23286 "regtype" => CastTarget::RegType,
23287 "regclass" => CastTarget::RegClass,
23288 // v7.12.0 — `::tsvector` / `::tsquery`.
23289 // Engine decodes the LHS text via the PG
23290 // external form parser.
23291 // v7.39 (round 352, M8) — MySQL's own cast targets.
23292 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23293 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23294 // such type, so they are taken only in that dialect and
23295 // fall through to the "type does not exist" arm otherwise.
23296 "signed" | "unsigned" if self.mysql_dialect => {
23297 if matches!(self.peek(), Token::Ident(k)
23298 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23299 {
23300 self.advance();
23301 }
23302 CastTarget::Named(s.to_ascii_lowercase())
23303 }
23304 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23305 // in MySQL: MariaDB answers '123' where the SQL-standard
23306 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23307 // Truncating a number to its first digit is a wrong answer
23308 // with no error, so the MySQL session gets MySQL's reading.
23309 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23310 CastTarget::Text
23311 }
23312 "tsvector" => CastTarget::TsVector,
23313 "tsquery" => CastTarget::TsQuery,
23314 // v7.17.0 — `::uuid`. Engine decodes the LHS
23315 // text via `spg_storage::parse_uuid_str`.
23316 "uuid" => CastTarget::Uuid,
23317 // v7.18 — `::bytea`. Engine decodes the LHS
23318 // text via the PG hex form (`'\xdeadbeef'`)
23319 // or escape form (`'\\x05\\x00'`). Closes
23320 // mailrs D-pre #3 reverse-acceptance gap.
23321 "bytea" => CastTarget::Bytea,
23322 // v7.37.5 ship triage — generic typed-cast escape.
23323 // Anything the long-tail PG type ident table knows
23324 // about(network/bit/geometry/multirange/etc.)flows
23325 // through `CastTarget::Named(canonical)`; the engine
23326 // resolves via `column_type_to_data_type` and dispatches
23327 // through the typed `coerce_value` path. Truly
23328 // unrecognised idents still hit the error arm below
23329 // because the engine rejects them.
23330 other => {
23331 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23332 // `::varchar(255)`, etc. Capture into the canonical
23333 // `name(p,s)` form so `type_name_to_data_type` can
23334 // reconstruct the `DataType::Numeric { precision,
23335 // scale }` (and similar param-carrying types).
23336 let mut name = other.to_string();
23337 // v7.39 (round 281) — `::bit varying(3)` is two
23338 // words; fold the tail in so the typmod reaches the
23339 // type resolver instead of tripping the parser.
23340 if name.eq_ignore_ascii_case("bit")
23341 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23342 {
23343 self.advance();
23344 name = alloc::string::String::from("varbit");
23345 }
23346 // v7.39 (round 613) — `::character varying` is the same
23347 // two-word shape and had no fold, so the `varying` was
23348 // left behind and the cast became a bare `character`,
23349 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23350 // `a` where PG answers `ab`. Silently, and for a spelling
23351 // pg_dump writes.
23352 if name.eq_ignore_ascii_case("character")
23353 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23354 {
23355 self.advance();
23356 name = alloc::string::String::from("varchar");
23357 }
23358 if matches!(self.peek(), Token::LParen) {
23359 let mut buf = alloc::string::String::from("(");
23360 let mut depth = 0usize;
23361 loop {
23362 match self.advance() {
23363 Token::LParen => {
23364 depth += 1;
23365 if depth > 1 {
23366 buf.push('(');
23367 }
23368 }
23369 Token::RParen => {
23370 depth -= 1;
23371 if depth == 0 {
23372 buf.push(')');
23373 break;
23374 }
23375 buf.push(')');
23376 }
23377 Token::Comma => buf.push(','),
23378 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23379 // v7.39 (round 273) — a minus used to fall
23380 // into the catch-all below and vanish, so
23381 // `::numeric(10,-2)` reached the engine as
23382 // the text `numeric(10,2)` and silently
23383 // rounded to two DECIMALS instead of to
23384 // hundreds. A dropped token is not a
23385 // no-op when it carries a sign.
23386 Token::Minus => buf.push('-'),
23387 Token::Eof => break,
23388 _ => {}
23389 }
23390 }
23391 name.push_str(&buf);
23392 }
23393 // Optional postfix `[]` widens to the array form —
23394 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23395 // The engine's `type_name_to_data_type` recognises
23396 // the canonical `<ty>_array` form.
23397 if matches!(self.peek(), Token::LBracket)
23398 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23399 {
23400 self.advance();
23401 self.advance();
23402 name.push_str("_array");
23403 }
23404 CastTarget::Named(name)
23405 }
23406 },
23407 Token::Interval => CastTarget::Interval,
23408 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23409 // "char" (oid 18, SPG Char1 — distinct from bare `char`
23410 // = char(1)); other quoted names resolve like idents.
23411 Token::QuotedIdent(q) => {
23412 if q.eq_ignore_ascii_case("char") {
23413 CastTarget::Named("char1".into())
23414 } else {
23415 CastTarget::Named(q.to_ascii_lowercase())
23416 }
23417 }
23418 other => {
23419 return Err(ParseError {
23420 message: format!("expected type ident after `::`, got {other:?}"),
23421 token_pos: self.consumed_pos(),
23422 });
23423 }
23424 };
23425 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23426 // target to its array sibling. Closed-enum arms (Bool /
23427 // SmallInt / Numeric / Float / Date / …) didn't carry the
23428 // explicit widening that Text / Int / BigInt did, so
23429 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23430 // error. The widening here mirrors the per-arm Text /
23431 // Int / BigInt logic above + folds the new ζ-A first-class
23432 // types through `CastTarget::Named("<ty>_array")`.
23433 if matches!(self.peek(), Token::LBracket)
23434 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23435 {
23436 let widened = match &target {
23437 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23438 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23439 // v7.39 (round 326, V43) — the two temporal types stay
23440 // distinct. Both used to widen to `timestamptz_array`, so
23441 // `::timestamp[]` named the wrong target in its own error
23442 // message and lost the zone-less identity on the way.
23443 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23444 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23445 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23446 CastTarget::Json | CastTarget::Jsonb => {
23447 Some(CastTarget::Named("jsonb_array".to_string()))
23448 }
23449 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23450 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23451 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23452 CastTarget::Named(name) => {
23453 let mut a = name.clone();
23454 a.push_str("_array");
23455 Some(CastTarget::Named(a))
23456 }
23457 // Int / BigInt / Text / Vector / TsVector / TsQuery /
23458 // RegType / RegClass / TextArray / IntArray /
23459 // BigIntArray already finalised — leave as is.
23460 _ => None,
23461 };
23462 if let Some(w) = widened {
23463 self.advance();
23464 self.advance();
23465 return Ok(w);
23466 }
23467 }
23468 Ok(target)
23469 }
23470
23471 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23472 loop {
23473 // v7.38 (read01, T9) — composite field access `(expr).field`.
23474 // A bare `a.b` is consumed as a qualified column inside the ident
23475 // atom, so a Dot only survives to this postfix position when the
23476 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23477 // `.*` whole-row expansion is not handled here (projection-level).
23478 if matches!(self.peek(), Token::Dot)
23479 && matches!(
23480 self.tokens.get(self.pos + 1),
23481 Some(Token::Ident(_) | Token::QuotedIdent(_))
23482 )
23483 {
23484 self.advance(); // .
23485 let field = match self.advance() {
23486 Token::Ident(s) | Token::QuotedIdent(s) => s,
23487 other => {
23488 return Err(
23489 self.err(format!("expected a field name after '.', got {other:?}"))
23490 );
23491 }
23492 };
23493 expr = Expr::FieldAccess {
23494 base: Box::new(expr),
23495 field,
23496 };
23497 continue;
23498 }
23499 if matches!(self.peek(), Token::DoubleColon) {
23500 self.advance();
23501 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23502 // target set to include INTERVAL (reserved Token),
23503 // TIMESTAMPTZ, and PG catalog regtype / regclass.
23504 // mailrs follow-up H3a + H3b.
23505 let target = self.parse_cast_target()?;
23506 expr = Expr::Cast {
23507 expr: Box::new(expr),
23508 target,
23509 };
23510 continue;
23511 }
23512 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23513 // returns NULL for out-of-range. Multiple subscripts
23514 // chain: `a[i][j]` parses left-to-right.
23515 if matches!(self.peek(), Token::LBracket) {
23516 self.advance();
23517 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23518 // bare index stays a subscript.
23519 let lo = if matches!(self.peek(), Token::Colon) {
23520 None
23521 } else {
23522 Some(self.parse_expr(0)?)
23523 };
23524 if matches!(self.peek(), Token::Colon) {
23525 self.advance();
23526 let hi = if matches!(self.peek(), Token::RBracket) {
23527 None
23528 } else {
23529 Some(Box::new(self.parse_expr(0)?))
23530 };
23531 if !matches!(self.peek(), Token::RBracket) {
23532 return Err(self.err(alloc::format!(
23533 "expected ']' after array slice, got {:?}",
23534 self.peek()
23535 )));
23536 }
23537 self.advance();
23538 expr = Expr::ArraySlice {
23539 target: Box::new(expr),
23540 lo: lo.map(Box::new),
23541 hi,
23542 };
23543 continue;
23544 }
23545 let index = lo.expect("non-colon branch parsed an index");
23546 if !matches!(self.peek(), Token::RBracket) {
23547 return Err(self.err(alloc::format!(
23548 "expected ']' after array index, got {:?}",
23549 self.peek()
23550 )));
23551 }
23552 self.advance();
23553 expr = Expr::ArraySubscript {
23554 target: Box::new(expr),
23555 index: Box::new(index),
23556 };
23557 continue;
23558 }
23559 // `expr AT TIME ZONE zone` — lowers to PG's own function
23560 // form timezone(zone, expr); the scalar implements the
23561 // offset shift (named zones error there — no tzdata).
23562 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23563 && matches!(self.tokens.get(self.pos + 1),
23564 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23565 && matches!(self.tokens.get(self.pos + 2),
23566 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23567 {
23568 self.advance(); // AT
23569 self.advance(); // TIME
23570 self.advance(); // ZONE
23571 // Zone at comparison precedence so AND/OR stay out.
23572 let zone = self.parse_expr(6)?;
23573 expr = Expr::FunctionCall {
23574 name: "timezone".to_string(),
23575 args: alloc::vec![zone, expr],
23576 };
23577 continue;
23578 }
23579 // `expr COLLATE "name"` — SPG's single text ordering IS
23580 // byte order, i.e. the C collation. The byte-order
23581 // spellings absorb as no-ops; a locale collation would
23582 // silently sort differently from PG, so it errors
23583 // honestly instead.
23584 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23585 self.advance();
23586 let mut cname = match self.advance() {
23587 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23588 other => {
23589 return Err(self.err(alloc::format!(
23590 "expected collation name after COLLATE, got {other:?}"
23591 )));
23592 }
23593 };
23594 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23595 // is how `pg_dump` writes the default one:
23596 // `… COLLATE pg_catalog.default`. Reading a single token
23597 // left the SCHEMA as the name, so the clause was refused
23598 // as an unsupported locale collation and no dump ran.
23599 if matches!(self.peek(), Token::Dot) {
23600 // v7.39.2 — the qualifier is DROPPED (SPG is single
23601 // schema) but it is checked first. PostgreSQL 18.6
23602 // answers `schema "nosuch_schema" does not exist` for
23603 // one it has never heard of, and dropping it unread
23604 // meant `COLLATE nosuch_schema."C"` succeeded here —
23605 // a name that names nothing, accepted.
23606 let schema = cname.to_ascii_lowercase();
23607 if !matches!(
23608 schema.as_str(),
23609 "pg_catalog" | "public" | "information_schema"
23610 ) {
23611 return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23612 }
23613 self.advance();
23614 cname = match self.advance() {
23615 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23616 // `default` lexes as a KEYWORD, and it is the name
23617 // pg_dump writes — the same trap round 535 hit with
23618 // TABLE / INDEX / FULL.
23619 Token::Default => alloc::string::String::from("default"),
23620 other => {
23621 return Err(self.err(alloc::format!(
23622 "expected collation name after COLLATE, got {other:?}"
23623 )));
23624 }
23625 };
23626 }
23627 let lc = cname.to_ascii_lowercase();
23628 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23629 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23630 // family / `binary`) forces byte-wise, which is exactly
23631 // what `BINARY expr` does — lower onto that so every fold
23632 // site (comparison, LIKE, ORDER BY) suppresses via
23633 // `is_binary_coerced`. A `_ci` family override folds, and
23634 // under the MySQL dialect the default already folds, so it
23635 // absorbs as a no-op; likewise the C / byte-order spellings.
23636 // v7.39.2 — against MySQL's own list, not against the
23637 // shape of the name. `nosuch_bin` took this shortcut and
23638 // became a BINARY cast; `nosuch_ci` took the one below
23639 // and was absorbed as a no-op. Either way the client
23640 // named a collation that does not exist and was told
23641 // nothing. An unknown name now falls through to the
23642 // node, and the engine refuses it.
23643 let real = crate::charset::is_mysql_collation(&lc);
23644 // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23645 //
23646 // They are not the same thing, and folding them together
23647 // lost a bit. Measured on MySQL 9.7.2 with the connection
23648 // on utf8mb4:
23649 //
23650 // ```text
23651 // 'a ' = 'a' COLLATE utf8mb4_bin 1 PAD SPACE
23652 // 'a ' = 'a' COLLATE utf8mb4_0900_bin 0 NO PAD
23653 // 'AB' = 'ab' COLLATE utf8mb4_bin 0 byte-wise
23654 // ```
23655 //
23656 // The BINARY cast carries "byte-wise" and, with it,
23657 // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23658 // the first line. Keeping the node lets `text_compare_of`
23659 // read the NAME and settle the two bits separately: it
23660 // does not fold (`folds_case` says so) and it does pad
23661 // (`pads_space` says so), while `is_byte_wise` still
23662 // keeps the ORDERING off the locale.
23663 if self.mysql_dialect && real && lc == "binary" {
23664 expr = Expr::Cast {
23665 expr: alloc::boxed::Box::new(expr),
23666 target: CastTarget::Named("binary".to_string()),
23667 };
23668 continue;
23669 }
23670 let mysql_ci = self.mysql_dialect
23671 && ((real && lc.ends_with("_ci"))
23672 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23673 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23674 // goes to the lowering channel, the byte-order spellings
23675 // included. Round 691 recorded only the names the old
23676 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23677 // absorbed as a no-op — and once a column could declare a
23678 // collation, absorbing the clause meant the COLUMN's
23679 // collation won where the query had asked for bytes.
23680 if self.in_order_by_key && !mysql_ci {
23681 self.order_key_collation = Some(cname);
23682 continue;
23683 }
23684 // v7.39.2 — the clause becomes a NODE rather than being
23685 // refused or absorbed.
23686 //
23687 // What stood here refused the locale names and SILENTLY
23688 // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23689 // answered `t` where PostgreSQL 18.6 answers `f`: the one
23690 // family it let through is the one where dropping it
23691 // changes the answer. Absorbing is only correct when the
23692 // collation asked for is the one the comparison would use
23693 // anyway, and that depends on the DATABASE — which the
23694 // parser cannot see. So it rides along and the engine,
23695 // which can, decides.
23696 //
23697 // `collate_derive` already modelled `Explicit(name)` and
23698 // had no way to be handed one.
23699 // v7.39.2 — a MySQL spelling does not exist on the
23700 // PostgreSQL wire, and THIS is where the wire is known.
23701 //
23702 // The check lived in the evaluator first and asked
23703 // `ctx.mysql_dialect`, which the INSERT path builds as a
23704 // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23705 // in a MySQL session was refused for a collation that
23706 // does not exist on a wire it was not on. Making that
23707 // context truthful would change INSERT-time evaluation
23708 // in other ways as a side effect; the parser already
23709 // gates the introducer on the same flag and is the
23710 // honest place to ask.
23711 if !self.mysql_dialect
23712 && (lc.ends_with("_ci")
23713 || lc.ends_with("_cs")
23714 || lc.ends_with("_bin")
23715 || lc == "binary"
23716 || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23717 {
23718 return Err(self.err(alloc::format!(
23719 "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23720 )));
23721 }
23722 // v7.39.3 — the node is built for EVERY name, `_ci`
23723 // included.
23724 //
23725 // A MySQL `_ci` spelling used to be absorbed here on the
23726 // reasoning that a MySQL session folds anyway, so the
23727 // clause asked for what it would have got. That stopped
23728 // being true when the fold learned to read the session's
23729 // collation NAME: under `SET NAMES utf8mb4 COLLATE
23730 // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23731 // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23732 // that would have made it 1 had been dropped in the
23733 // parser. Absorbing is only ever correct when the
23734 // collation asked for is the one the comparison would use
23735 // anyway, and the parser cannot know that — the same
23736 // reasoning already written above for the byte-order
23737 // spellings, applied to the family it had exempted.
23738 expr = Expr::Collate {
23739 expr: alloc::boxed::Box::new(expr),
23740 collation: cname,
23741 };
23742 continue;
23743 }
23744 return Ok(expr);
23745 }
23746 }
23747
23748 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23749 /// the first token that is not one. Schema qualifiers collapse to the
23750 /// last part, which is what every other name path here does (SPG is
23751 /// single-schema).
23752 fn take_comma_separated_names(&mut self) -> Vec<String> {
23753 let mut out = Vec::new();
23754 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23755 self.advance();
23756 let mut last = n;
23757 while matches!(self.peek(), Token::Dot) {
23758 self.advance();
23759 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23760 last = t;
23761 }
23762 }
23763 out.push(last);
23764 if matches!(self.peek(), Token::Comma) {
23765 self.advance();
23766 } else {
23767 break;
23768 }
23769 }
23770 out
23771 }
23772
23773 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23774 ///
23775 /// The general cast-target path tests this inline; the types with their
23776 /// own `CastTarget` variant need it as a guard on their match arm,
23777 /// which is what this exists for.
23778 fn peek_postfix_array_brackets(&self) -> bool {
23779 matches!(self.peek(), Token::LBracket)
23780 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23781 }
23782
23783 /// Parse the operator tail after a `(a, b, …)` row constructor
23784 /// and expand at parse time. `=` is the conjunction of element
23785 /// equalities; `<>` its negation; the order operators expand
23786 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23787 /// equalities. Anything else (a bare row value, a subquery
23788 /// RHS) errors honestly — SPG has no composite runtime value.
23789 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23790 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23791 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23792 lhs: Box::new(l.clone()),
23793 op: BinOp::Eq,
23794 rhs: Box::new(r.clone()),
23795 });
23796 let first = it.next().expect("row has at least two elements");
23797 it.fold(first, |acc, e| Expr::Binary {
23798 lhs: Box::new(acc),
23799 op: BinOp::And,
23800 rhs: Box::new(e),
23801 })
23802 }
23803 // Lexicographic (a,b) OP (c,d):
23804 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
23805 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23806 if lhs.len() == 1 {
23807 return Expr::Binary {
23808 lhs: Box::new(lhs[0].clone()),
23809 op: last,
23810 rhs: Box::new(rhs[0].clone()),
23811 };
23812 }
23813 let head_strict = Expr::Binary {
23814 lhs: Box::new(lhs[0].clone()),
23815 op: strict,
23816 rhs: Box::new(rhs[0].clone()),
23817 };
23818 let head_eq = Expr::Binary {
23819 lhs: Box::new(lhs[0].clone()),
23820 op: BinOp::Eq,
23821 rhs: Box::new(rhs[0].clone()),
23822 };
23823 Expr::Binary {
23824 lhs: Box::new(head_strict),
23825 op: BinOp::Or,
23826 rhs: Box::new(Expr::Binary {
23827 lhs: Box::new(head_eq),
23828 op: BinOp::And,
23829 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23830 }),
23831 }
23832 }
23833 let negated_in = if matches!(self.peek(), Token::Not)
23834 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23835 {
23836 self.advance();
23837 true
23838 } else {
23839 false
23840 };
23841 if matches!(self.peek(), Token::In) {
23842 self.advance();
23843 if !matches!(self.peek(), Token::LParen) {
23844 return Err(self.err(alloc::format!(
23845 "expected '(' after row IN, got {:?}",
23846 self.peek()
23847 )));
23848 }
23849 self.advance();
23850 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23851 // not a list of literal rows. Row-vs-list decomposes to
23852 // OR-of-AND above, but the subquery's rows are only known at
23853 // runtime, so keep it as a RowInSubquery node.
23854 if matches!(self.peek(), Token::Select) {
23855 let inner = self.parse_select_stmt()?;
23856 if !matches!(self.peek(), Token::RParen) {
23857 return Err(self.err(alloc::format!(
23858 "expected ')' after row IN-subquery, got {:?}",
23859 self.peek()
23860 )));
23861 }
23862 self.advance();
23863 let Statement::Select(s) = inner else {
23864 unreachable!("parse_select_stmt always returns Statement::Select")
23865 };
23866 return Ok(Expr::RowInSubquery {
23867 row,
23868 subquery: Box::new(s),
23869 negated: negated_in,
23870 });
23871 }
23872 let mut alternatives: Vec<Expr> = Vec::new();
23873 loop {
23874 // Optional ROW keyword before the paren row.
23875 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23876 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23877 {
23878 self.advance();
23879 }
23880 if !matches!(self.peek(), Token::LParen) {
23881 return Err(self.err(alloc::format!(
23882 "expected '(' to open a row inside IN, got {:?}",
23883 self.peek()
23884 )));
23885 }
23886 self.advance();
23887 let mut rhs = alloc::vec![self.parse_expr(0)?];
23888 while matches!(self.peek(), Token::Comma) {
23889 self.advance();
23890 rhs.push(self.parse_expr(0)?);
23891 }
23892 if !matches!(self.peek(), Token::RParen) {
23893 return Err(self.err(alloc::format!(
23894 "expected ')' after row inside IN, got {:?}",
23895 self.peek()
23896 )));
23897 }
23898 self.advance();
23899 if rhs.len() != row.len() {
23900 return Err(self.err(alloc::format!(
23901 "row IN arity mismatch: left has {}, right has {}",
23902 row.len(),
23903 rhs.len()
23904 )));
23905 }
23906 alternatives.push(row_eq(&row, &rhs));
23907 if matches!(self.peek(), Token::Comma) {
23908 self.advance();
23909 continue;
23910 }
23911 break;
23912 }
23913 if !matches!(self.peek(), Token::RParen) {
23914 return Err(self.err(alloc::format!(
23915 "expected ')' to close row IN list, got {:?}",
23916 self.peek()
23917 )));
23918 }
23919 self.advance();
23920 let mut it = alternatives.into_iter();
23921 let first = it.next().expect("IN list has at least one row");
23922 let combined = it.fold(first, |acc, e| Expr::Binary {
23923 lhs: Box::new(acc),
23924 op: BinOp::Or,
23925 rhs: Box::new(e),
23926 });
23927 return Ok(maybe_not(combined, negated_in));
23928 }
23929 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23930 // two periods share at least one time point. Each pair is
23931 // normalised with least/greatest (PG accepts the endpoints
23932 // in either order), then lowered to the standard
23933 // `start1 < end2 AND start2 < end1` form.
23934 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23935 if row.len() != 2 {
23936 return Err(self.err(alloc::format!(
23937 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23938 row.len()
23939 )));
23940 }
23941 self.advance();
23942 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23943 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23944 {
23945 self.advance();
23946 }
23947 if !matches!(self.peek(), Token::LParen) {
23948 return Err(self.err(alloc::format!(
23949 "expected '(' after OVERLAPS, got {:?}",
23950 self.peek()
23951 )));
23952 }
23953 self.advance();
23954 let r0 = self.parse_expr(0)?;
23955 if !matches!(self.peek(), Token::Comma) {
23956 return Err(self.err(alloc::format!(
23957 "OVERLAPS needs (start, end) on the right, got {:?}",
23958 self.peek()
23959 )));
23960 }
23961 self.advance();
23962 let r1 = self.parse_expr(0)?;
23963 if !matches!(self.peek(), Token::RParen) {
23964 return Err(self.err(alloc::format!(
23965 "expected ')' after OVERLAPS pair, got {:?}",
23966 self.peek()
23967 )));
23968 }
23969 self.advance();
23970 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23971 name: String::from(name),
23972 args: alloc::vec![a.clone(), b.clone()],
23973 };
23974 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23975 lhs: Box::new(lhs),
23976 op: BinOp::Lt,
23977 rhs: Box::new(rhs),
23978 };
23979 return Ok(Expr::Binary {
23980 lhs: Box::new(lt(
23981 pair_fn("least", &row[0], &row[1]),
23982 pair_fn("greatest", &r0, &r1),
23983 )),
23984 op: BinOp::And,
23985 rhs: Box::new(lt(
23986 pair_fn("least", &r0, &r1),
23987 pair_fn("greatest", &row[0], &row[1]),
23988 )),
23989 });
23990 }
23991 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23992 // PG, `IS NULL` is true only when EVERY field is NULL, and
23993 // `IS NOT NULL` is true only when every field is non-NULL — the
23994 // latter is NOT the negation of the former (a mixed row is
23995 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23996 // which reproduces exactly that all-fields semantics.
23997 if matches!(self.peek(), Token::Is) {
23998 self.advance();
23999 let negated = if matches!(self.peek(), Token::Not) {
24000 self.advance();
24001 true
24002 } else {
24003 false
24004 };
24005 if !matches!(self.peek(), Token::Null) {
24006 return Err(self.err(alloc::format!(
24007 "expected NULL after row IS [NOT], got {:?}",
24008 self.peek()
24009 )));
24010 }
24011 self.advance();
24012 let mut it = row.iter().map(|e| Expr::IsNull {
24013 expr: Box::new(e.clone()),
24014 negated,
24015 });
24016 let first = it.next().expect("row has at least two elements");
24017 return Ok(it.fold(first, |acc, e| Expr::Binary {
24018 lhs: Box::new(acc),
24019 op: BinOp::And,
24020 rhs: Box::new(e),
24021 }));
24022 }
24023 let op = match self.peek() {
24024 Token::Eq => BinOp::Eq,
24025 Token::NotEq => BinOp::NotEq,
24026 Token::Lt => BinOp::Lt,
24027 Token::LtEq => BinOp::LtEq,
24028 Token::Gt => BinOp::Gt,
24029 Token::GtEq => BinOp::GtEq,
24030 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
24031 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
24032 // constructor value, identical to the `ROW(a, b, …)` keyword form:
24033 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
24034 // (`::text`, `.field`) applies at the caller just as it does for the
24035 // ROW(...) node. All the comparison / predicate forms returned above.
24036 _ => {
24037 return Ok(Expr::FunctionCall {
24038 name: String::from("row"),
24039 args: row,
24040 });
24041 }
24042 };
24043 self.advance();
24044 // Optional ROW keyword before the paren row.
24045 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
24046 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
24047 {
24048 self.advance();
24049 }
24050 if !matches!(self.peek(), Token::LParen) {
24051 return Err(self.err(alloc::format!(
24052 "expected '(' to open the right-hand row, got {:?}",
24053 self.peek()
24054 )));
24055 }
24056 self.advance();
24057 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
24058 // subquery. Kept as a node (the subquery's row is a runtime value);
24059 // the literal-RHS form below still decomposes at parse time.
24060 if matches!(self.peek(), Token::Select) {
24061 let inner = self.parse_select_stmt()?;
24062 if !matches!(self.peek(), Token::RParen) {
24063 return Err(self.err(alloc::format!(
24064 "expected ')' after row comparison subquery, got {:?}",
24065 self.peek()
24066 )));
24067 }
24068 self.advance();
24069 let Statement::Select(s) = inner else {
24070 unreachable!("parse_select_stmt always returns Statement::Select")
24071 };
24072 return Ok(Expr::RowCmpSubquery {
24073 row,
24074 op,
24075 subquery: Box::new(s),
24076 });
24077 }
24078 let mut rhs = alloc::vec![self.parse_expr(0)?];
24079 while matches!(self.peek(), Token::Comma) {
24080 self.advance();
24081 rhs.push(self.parse_expr(0)?);
24082 }
24083 if !matches!(self.peek(), Token::RParen) {
24084 return Err(self.err(alloc::format!(
24085 "expected ')' after right-hand row, got {:?}",
24086 self.peek()
24087 )));
24088 }
24089 self.advance();
24090 if rhs.len() != row.len() {
24091 // v7.39 (round 239) — PG's wording (42601).
24092 return Err(self.err("unequal number of entries in row expressions".to_string()));
24093 }
24094 Ok(match op {
24095 BinOp::Eq => row_eq(&row, &rhs),
24096 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
24097 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
24098 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
24099 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
24100 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
24101 _ => unreachable!("op restricted above"),
24102 })
24103 }
24104
24105 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
24106 /// escape character becomes the matcher's default backslash:
24107 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
24108 /// → the char itself, and any pre-existing backslash escapes
24109 /// itself so it stays literal. Both operands must be string
24110 /// literals — a runtime pattern would need matcher support.
24111 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24112 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24113 (&pattern, &esc)
24114 else {
24115 return Err(
24116 "LIKE ... ESCAPE requires string-literal pattern and escape \
24117 (runtime escape characters are not supported yet)"
24118 .into(),
24119 );
24120 };
24121 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24122 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24123 // multi-character escape is an error.
24124 let esc_ch: Option<char> = {
24125 let mut ch_iter = e.chars();
24126 match (ch_iter.next(), ch_iter.next()) {
24127 (Some(c), None) => Some(c),
24128 (None, _) => None,
24129 (Some(_), Some(_)) => {
24130 return Err(alloc::format!(
24131 "ESCAPE must be a single character, got {e:?}"
24132 ));
24133 }
24134 }
24135 };
24136 let mut out = String::with_capacity(p.len() + 4);
24137 let mut chars = p.chars();
24138 while let Some(c) = chars.next() {
24139 if Some(c) == esc_ch {
24140 match chars.next() {
24141 // Escaped wildcard / escaped escape → keep the
24142 // next char literal via backslash.
24143 Some(next) => {
24144 out.push('\\');
24145 out.push(next);
24146 }
24147 None => {
24148 return Err("LIKE pattern ends with the escape character".into());
24149 }
24150 }
24151 } else if c == '\\' && esc_ch != Some('\\') {
24152 // A raw backslash is literal under a custom (or absent) escape
24153 // — escape it for the backslash-based matcher.
24154 out.push('\\');
24155 out.push('\\');
24156 } else {
24157 out.push(c);
24158 }
24159 }
24160 Ok(Expr::Literal(Literal::String(out)))
24161 }
24162
24163 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24164 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24165 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24166 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24167 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24168 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24169 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24170 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24171 /// array expression errors honestly rather than silently mismatching.
24172 fn try_like_any_all(
24173 &mut self,
24174 base: &Expr,
24175 negated: bool,
24176 case_insensitive: bool,
24177 ) -> Result<Option<Expr>, ParseError> {
24178 let is_any = match self.peek() {
24179 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24180 Token::Ident(s)
24181 if s.eq_ignore_ascii_case("any")
24182 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24183 {
24184 true
24185 }
24186 _ => return Ok(None),
24187 };
24188 self.advance(); // ANY / ALL
24189 self.advance(); // '('
24190 let arr = self.parse_expr(0)?;
24191 if !matches!(self.peek(), Token::RParen) {
24192 return Err(self.err(format!(
24193 "expected ')' after LIKE {} argument, got {:?}",
24194 if is_any { "ANY" } else { "ALL" },
24195 self.peek()
24196 )));
24197 }
24198 self.advance(); // ')'
24199 let Expr::Array(items) = arr else {
24200 return Err(self.err(
24201 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24202 ));
24203 };
24204 let mut clauses = items.into_iter().map(|p| Expr::Like {
24205 expr: Box::new(base.clone()),
24206 pattern: Box::new(p),
24207 negated,
24208 case_insensitive,
24209 });
24210 let Some(first) = clauses.next() else {
24211 // ANY(empty) = FALSE, ALL(empty) = TRUE.
24212 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24213 };
24214 let op = if is_any { BinOp::Or } else { BinOp::And };
24215 let combined = clauses.fold(first, |acc, c| Expr::Binary {
24216 lhs: Box::new(acc),
24217 op,
24218 rhs: Box::new(c),
24219 });
24220 Ok(Some(combined))
24221 }
24222
24223 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
24224 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24225 /// `AND` is not swallowed.
24226 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24227 self.advance(); // BETWEEN
24228 // SYMMETRIC — the bounds may arrive in either order; both
24229 // orientations OR together. ASYMMETRIC is the default and
24230 // absorbs as noise.
24231 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24232 {
24233 self.advance();
24234 true
24235 } else {
24236 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24237 self.advance();
24238 }
24239 false
24240 };
24241 let low = self.parse_expr(6)?;
24242 if !matches!(self.peek(), Token::And) {
24243 return Err(self.err(format!(
24244 "expected AND after BETWEEN low bound, got {:?}",
24245 self.peek()
24246 )));
24247 }
24248 self.advance();
24249 let high = self.parse_expr(6)?;
24250 let target = Box::new(expr);
24251 let range = |lo: Expr, hi: Expr| Expr::Binary {
24252 lhs: Box::new(Expr::Binary {
24253 lhs: target.clone(),
24254 op: BinOp::GtEq,
24255 rhs: Box::new(lo),
24256 }),
24257 op: BinOp::And,
24258 rhs: Box::new(Expr::Binary {
24259 lhs: target.clone(),
24260 op: BinOp::LtEq,
24261 rhs: Box::new(hi),
24262 }),
24263 };
24264 let combined = if symmetric {
24265 Expr::Binary {
24266 lhs: Box::new(range(low.clone(), high.clone())),
24267 op: BinOp::Or,
24268 rhs: Box::new(range(high, low)),
24269 }
24270 } else {
24271 range(low, high)
24272 };
24273 Ok(maybe_not(combined, negated))
24274 }
24275
24276 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
24277 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24278 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24279 /// Caller already consumed the leading `WITH` ident.
24280 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24281 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24282 /// self-reference that appears more than once in a single term.
24283 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24284 use crate::ast::{CteBody, SelectStatement};
24285 if !cte.recursive {
24286 return Ok(());
24287 }
24288 let CteBody::Select(body) = &cte.body else {
24289 return Ok(());
24290 };
24291 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24292 // check the anchor and every peer term.
24293 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24294 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24295 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24296 return Err(self.err(String::from(
24297 "ORDER BY in a recursive query is not implemented",
24298 )));
24299 }
24300 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24301 return Err(self.err(String::from(
24302 "LIMIT in a recursive query is not implemented",
24303 )));
24304 }
24305 let self_refs = |s: &SelectStatement| -> usize {
24306 let Some(from) = &s.from else {
24307 return 0;
24308 };
24309 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24310 for j in &from.joins {
24311 if j.table.name.eq_ignore_ascii_case(&cte.name) {
24312 n += 1;
24313 }
24314 }
24315 n
24316 };
24317 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24318 return Err(self.err(alloc::format!(
24319 "recursive reference to query \"{}\" must not appear more than once",
24320 cte.name
24321 )));
24322 }
24323 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24324 // apply only when the body actually references itself (a non-self-
24325 // referencing CTE under WITH RECURSIVE may use any set-op shape).
24326 let anchor_refs = self_refs(body);
24327 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24328 if anchor_refs > 0 || union_refs {
24329 // Shape: the top level must be UNION [ALL] arms only. A self-ref
24330 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24331 // "does not have the form" error — SPG used to compute a value.
24332 if body.unions.is_empty()
24333 || body.unions.iter().any(|(k, _)| {
24334 !matches!(
24335 k,
24336 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24337 )
24338 })
24339 {
24340 return Err(self.err(alloc::format!(
24341 "recursive query \"{}\" does not have the form non-recursive-term \
24342 UNION [ALL] recursive-term",
24343 cte.name
24344 )));
24345 }
24346 if anchor_refs > 0 {
24347 return Err(self.err(alloc::format!(
24348 "recursive reference to query \"{}\" must not appear within its non-recursive term",
24349 cte.name
24350 )));
24351 }
24352 }
24353 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24354 for (_, u) in &body.unions {
24355 if self_refs(u) == 0 {
24356 continue;
24357 }
24358 // The self-reference must not sit on the nullable side of an outer
24359 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24360 if let Some(from) = &u.from {
24361 for (i, j) in from.joins.iter().enumerate() {
24362 let left_has_self = is_self(&from.primary)
24363 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24364 let violated = match j.kind {
24365 crate::ast::JoinKind::Left => is_self(&j.table),
24366 crate::ast::JoinKind::Right => left_has_self,
24367 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24368 _ => false,
24369 };
24370 if violated {
24371 return Err(self.err(alloc::format!(
24372 "recursive reference to query \"{}\" must not appear within an outer join",
24373 cte.name
24374 )));
24375 }
24376 }
24377 }
24378 // No aggregates at the top level of the recursive term (SPG used
24379 // to run them and surface a misleading downstream error).
24380 let mut items_and_having: Vec<&Expr> = Vec::new();
24381 for it in &u.items {
24382 if let crate::ast::SelectItem::Expr { expr, .. } = it {
24383 items_and_having.push(expr);
24384 }
24385 }
24386 if let Some(h) = &u.having {
24387 items_and_having.push(h);
24388 }
24389 for e in items_and_having {
24390 if expr_has_toplevel_aggregate(e) {
24391 return Err(self.err(String::from(
24392 "aggregate functions are not allowed in a recursive query's recursive term",
24393 )));
24394 }
24395 }
24396 }
24397 // A self-reference inside a sublink expression (EXISTS / IN / scalar
24398 // subquery) anywhere in the body is rejected; a plain FROM derived
24399 // table is legal in PG and untouched here.
24400 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24401 all_terms.extend(body.unions.iter().map(|(_, u)| u));
24402 for term in all_terms {
24403 if select_has_self_ref_in_sublink(term, &cte.name) {
24404 return Err(self.err(alloc::format!(
24405 "recursive reference to query \"{}\" must not appear within a subquery",
24406 cte.name
24407 )));
24408 }
24409 }
24410 Ok(())
24411 }
24412
24413 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24414 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24415 /// right after parse so the engine sees a plain recursive CTE with the
24416 /// tracking columns already projected. DEPTH FIRST and CYCLE are
24417 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24418 /// text-rendered rows can't provide, and errors honestly.
24419 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24420 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24421 if cte.search.is_none() && cte.cycle.is_none() {
24422 return Ok(());
24423 }
24424 let cte_name = cte.name.clone();
24425 let col_names = cte.column_overrides.clone();
24426 if col_names.is_empty() {
24427 return Err(
24428 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24429 );
24430 }
24431 let search = cte.search.take();
24432 let cycle = cte.cycle.take();
24433 let mut extra_cols: Vec<String> = Vec::new();
24434 let col_ref = |name: &str| {
24435 Expr::Column(ColumnName {
24436 qualifier: Some(cte_name.clone()),
24437 name: name.to_string(),
24438 })
24439 };
24440 // Position of a SEARCH/CYCLE column within the CTE's column list.
24441 let pos_of = |name: &str| -> Result<usize, ParseError> {
24442 col_names
24443 .iter()
24444 .position(|c| c.eq_ignore_ascii_case(name))
24445 .ok_or_else(|| {
24446 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24447 })
24448 };
24449 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24450 let mut args = Vec::with_capacity(positions.len());
24451 for &p in positions {
24452 match items.get(p) {
24453 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24454 _ => {
24455 return Err(self.err(
24456 "SEARCH/CYCLE column maps to a non-expression select item".into(),
24457 ));
24458 }
24459 }
24460 }
24461 Ok(Expr::FunctionCall {
24462 name: "row".into(),
24463 args,
24464 })
24465 };
24466 let CteBody::Select(body) = &mut cte.body else {
24467 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24468 };
24469 if body.unions.is_empty() {
24470 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24471 }
24472 let rec = body.unions.len() - 1; // recursive term = last UNION peer
24473
24474 if let Some(srch) = search {
24475 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24476 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24477 // no typed `record[]`, but element-wise array ORDER BY is correct
24478 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24479 // exactly onto a typed array: DEPTH is the root→node path
24480 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24481 // orders numerically (multi-digit keys included), matching PG.
24482 //
24483 // A multi-column BY would need a record[] to keep the per-node key
24484 // tuple orderable, which SPG can't express — error honestly there
24485 // rather than mis-order.
24486 if srch.by_columns.len() != 1 {
24487 return Err(self.err(
24488 "SEARCH … BY with multiple columns needs typed record[] ordering \
24489 SPG doesn't have yet; a single BY column is supported"
24490 .into(),
24491 ));
24492 }
24493 let key_pos = pos_of(&srch.by_columns[0])?;
24494 let base_key = match body.items.get(key_pos) {
24495 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24496 _ => {
24497 return Err(
24498 self.err("SEARCH BY column maps to a non-expression select item".into())
24499 );
24500 }
24501 };
24502 let rec_key = match body.unions[rec].1.items.get(key_pos) {
24503 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24504 _ => {
24505 return Err(
24506 self.err("SEARCH BY column maps to a non-expression select item".into())
24507 );
24508 }
24509 };
24510 if srch.depth_first {
24511 // base: ARRAY[key]; rec: array_append(cte.set, key).
24512 body.items.push(SelectItem::Expr {
24513 expr: Expr::Array(alloc::vec![base_key]),
24514 alias: Some(srch.set_column.clone()),
24515 });
24516 body.unions[rec].1.items.push(SelectItem::Expr {
24517 expr: Expr::FunctionCall {
24518 name: "array_append".into(),
24519 args: alloc::vec![col_ref(&srch.set_column), rec_key],
24520 },
24521 alias: Some(srch.set_column.clone()),
24522 });
24523 } else {
24524 // BREADTH: [depth, key]; depth starts at 0 and increments. The
24525 // leading depth element dominates the element-wise comparison,
24526 // so shallower rows sort first, then by key — PG's (depth, key).
24527 body.items.push(SelectItem::Expr {
24528 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24529 alias: Some(srch.set_column.clone()),
24530 });
24531 // rec depth = cte.set[1] + 1.
24532 let parent_depth = Expr::ArraySubscript {
24533 target: Box::new(col_ref(&srch.set_column)),
24534 index: Box::new(Expr::Literal(Literal::Integer(1))),
24535 };
24536 body.unions[rec].1.items.push(SelectItem::Expr {
24537 expr: Expr::Array(alloc::vec![
24538 Expr::Binary {
24539 lhs: Box::new(parent_depth),
24540 op: BinOp::Add,
24541 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24542 },
24543 rec_key,
24544 ]),
24545 alias: Some(srch.set_column.clone()),
24546 });
24547 }
24548 extra_cols.push(srch.set_column);
24549 }
24550
24551 if let Some(cyc) = cycle {
24552 let positions: Vec<usize> = cyc
24553 .columns
24554 .iter()
24555 .map(|c| pos_of(c))
24556 .collect::<Result<_, _>>()?;
24557 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24558 // cast it to text for the cycle path: membership only needs equality,
24559 // and the record text form gives SPG a TextArray path (SPG has no
24560 // typed record[] array). Cycle detection is unaffected.
24561 let base_row = Expr::Cast {
24562 expr: Box::new(row_of(&body.items, &positions)?),
24563 target: CastTarget::Text,
24564 };
24565 let rec_row = Expr::Cast {
24566 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24567 target: CastTarget::Text,
24568 };
24569 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24570 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24571 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24572 body.items.push(SelectItem::Expr {
24573 expr: Expr::Literal(dflt.clone()),
24574 alias: Some(cyc.mark_column.clone()),
24575 });
24576 body.items.push(SelectItem::Expr {
24577 expr: Expr::Array(alloc::vec![base_row]),
24578 alias: Some(cyc.path_column.clone()),
24579 });
24580 // rec mark: ROW(cols) already in the path → cycle.
24581 let hit = Expr::AnyAll {
24582 expr: Box::new(rec_row.clone()),
24583 op: BinOp::Eq,
24584 array: Box::new(col_ref(&cyc.path_column)),
24585 is_any: true,
24586 };
24587 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24588 Expr::Case {
24589 operand: None,
24590 branches: alloc::vec![(hit, Expr::Literal(mark))],
24591 else_branch: Some(Box::new(Expr::Literal(dflt))),
24592 }
24593 } else {
24594 hit
24595 };
24596 body.unions[rec].1.items.push(SelectItem::Expr {
24597 expr: mark_expr,
24598 alias: Some(cyc.mark_column.clone()),
24599 });
24600 // rec path: array_append(cte.path, ROW(cols)).
24601 body.unions[rec].1.items.push(SelectItem::Expr {
24602 expr: Expr::FunctionCall {
24603 name: "array_append".into(),
24604 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24605 },
24606 alias: Some(cyc.path_column.clone()),
24607 });
24608 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24609 let stop = Expr::Unary {
24610 op: UnOp::Not,
24611 expr: Box::new(col_ref(&cyc.mark_column)),
24612 };
24613 let w = &mut body.unions[rec].1.where_;
24614 *w = Some(match w.take() {
24615 Some(prev) => Expr::Binary {
24616 lhs: Box::new(prev),
24617 op: BinOp::And,
24618 rhs: Box::new(stop),
24619 },
24620 None => stop,
24621 });
24622 extra_cols.push(cyc.mark_column);
24623 extra_cols.push(cyc.path_column);
24624 }
24625 cte.column_overrides.extend(extra_cols);
24626 Ok(())
24627 }
24628
24629 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24630 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24631 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24632 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24633 return Ok(None);
24634 }
24635 self.advance(); // SEARCH
24636 let depth_first = match self.peek() {
24637 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24638 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24639 other => {
24640 return Err(self.err(format!(
24641 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24642 )));
24643 }
24644 };
24645 self.advance();
24646 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24647 return Err(self.err(format!(
24648 "expected FIRST after SEARCH mode, got {:?}",
24649 self.peek()
24650 )));
24651 }
24652 self.advance();
24653 if !self.peek_is_by() {
24654 return Err(self.err(format!(
24655 "expected BY after SEARCH … FIRST, got {:?}",
24656 self.peek()
24657 )));
24658 }
24659 self.advance();
24660 let mut by_columns = alloc::vec![self.expect_ident_like()?];
24661 while matches!(self.peek(), Token::Comma) {
24662 self.advance();
24663 by_columns.push(self.expect_ident_like()?);
24664 }
24665 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24666 return Err(self.err(format!(
24667 "expected SET in SEARCH clause, got {:?}",
24668 self.peek()
24669 )));
24670 }
24671 self.advance();
24672 let set_column = self.expect_ident_like()?;
24673 Ok(Some(crate::ast::SearchClause {
24674 depth_first,
24675 by_columns,
24676 set_column,
24677 }))
24678 }
24679
24680 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24681 /// USING pathcol`. Returns None when the next token isn't CYCLE.
24682 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24683 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24684 return Ok(None);
24685 }
24686 self.advance(); // CYCLE
24687 let mut columns = alloc::vec![self.expect_ident_like()?];
24688 while matches!(self.peek(), Token::Comma) {
24689 self.advance();
24690 columns.push(self.expect_ident_like()?);
24691 }
24692 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24693 return Err(self.err(format!(
24694 "expected SET in CYCLE clause, got {:?}",
24695 self.peek()
24696 )));
24697 }
24698 self.advance();
24699 let mark_column = self.expect_ident_like()?;
24700 let mut mark_value = None;
24701 let mut default_value = None;
24702 if matches!(self.peek(), Token::To) {
24703 self.advance();
24704 mark_value = Some(self.parse_cycle_literal()?);
24705 if !matches!(self.peek(), Token::Default) {
24706 return Err(self.err(format!(
24707 "expected DEFAULT after CYCLE … TO, got {:?}",
24708 self.peek()
24709 )));
24710 }
24711 self.advance();
24712 default_value = Some(self.parse_cycle_literal()?);
24713 }
24714 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24715 return Err(self.err(format!(
24716 "expected USING in CYCLE clause, got {:?}",
24717 self.peek()
24718 )));
24719 }
24720 self.advance();
24721 let path_column = self.expect_ident_like()?;
24722 Ok(Some(crate::ast::CycleClause {
24723 columns,
24724 mark_column,
24725 mark_value,
24726 default_value,
24727 path_column,
24728 }))
24729 }
24730
24731 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24732 /// literal (string / bool / number) in PG.
24733 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24734 match self.parse_expr(0)? {
24735 Expr::Literal(l) => Ok(l),
24736 other => Err(self.err(format!(
24737 "CYCLE mark/default value must be a literal, got {other:?}"
24738 ))),
24739 }
24740 }
24741
24742 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24743 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24744 // Comes through as an identifier; consume it if present and
24745 // mark every CTE in the clause as recursive (PG semantics —
24746 // the flag is per-WITH, not per-CTE).
24747 let mut recursive = false;
24748 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24749 && s.eq_ignore_ascii_case("recursive")
24750 {
24751 self.advance();
24752 recursive = true;
24753 }
24754 let mut ctes = Vec::new();
24755 loop {
24756 let name = self.expect_ident_like()?;
24757 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24758 // PG uses these to rename the body's output columns; we
24759 // do the same below by overriding `columns[i].name`.
24760 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24761 self.advance();
24762 let mut names = Vec::new();
24763 loop {
24764 names.push(self.expect_ident_like()?);
24765 if matches!(self.peek(), Token::Comma) {
24766 self.advance();
24767 continue;
24768 }
24769 break;
24770 }
24771 if !matches!(self.peek(), Token::RParen) {
24772 return Err(self.err(format!(
24773 "expected ')' to close CTE column list, got {:?}",
24774 self.peek()
24775 )));
24776 }
24777 self.advance();
24778 names
24779 } else {
24780 Vec::new()
24781 };
24782 // AS is a reserved Token::As (used by SELECT-item / FROM
24783 // aliasing) — handle it specially rather than as a bare
24784 // ident.
24785 if !matches!(self.peek(), Token::As) {
24786 return Err(self.err(format!(
24787 "expected AS after CTE name {name:?}, got {:?}",
24788 self.peek()
24789 )));
24790 }
24791 self.advance();
24792 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24793 // MATERIALIZED` optimizer hints. SPG materialises every
24794 // CTE, so both spellings are accepted and absorbed.
24795 if matches!(self.peek(), Token::Not) {
24796 self.advance(); // NOT
24797 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24798 if s.eq_ignore_ascii_case("materialized"))
24799 {
24800 self.advance();
24801 } else {
24802 return Err(self.err(format!(
24803 "expected MATERIALIZED after AS NOT, got {:?}",
24804 self.peek()
24805 )));
24806 }
24807 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24808 if s.eq_ignore_ascii_case("materialized"))
24809 {
24810 self.advance();
24811 }
24812 if !matches!(self.peek(), Token::LParen) {
24813 return Err(self.err(format!(
24814 "expected '(' after AS in WITH clause, got {:?}",
24815 self.peek()
24816 )));
24817 }
24818 self.advance();
24819 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24820 // RETURNING) as the CTE body in addition to SELECT.
24821 // PG writable CTE semantics. UPDATE / DELETE come in as
24822 // bare Idents (lexer keeps SELECT / INSERT as reserved
24823 // tokens but treats the rest of DML as case-insensitive
24824 // idents).
24825 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24826 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24827 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24828 let body = match self.peek() {
24829 Token::Select => {
24830 let inner = self.parse_select_stmt()?;
24831 let Statement::Select(s) = inner else {
24832 unreachable!("parse_select_stmt returns Select");
24833 };
24834 crate::ast::CteBody::Select(s)
24835 }
24836 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24837 // `SELECT * FROM t` this way and accepts it wherever a
24838 // SELECT goes, so the CTE body dispatch needs its own
24839 // arm: this match is keyed on the FIRST token, and
24840 // `Token::Table` fell through to a tail that then
24841 // rejected what it got. `parse_table_shorthand` has
24842 // returned a desugared SelectStatement since the
24843 // shorthand landed — only the routing was missing.
24844 // Round 868 found this by putting the shorthand in a
24845 // subquery; every earlier check used a top-level form.
24846 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24847 // `SELECT * FROM t` this way and accepts it wherever a
24848 // SELECT goes, so the CTE body dispatch needs its own
24849 // arm: this match is keyed on the FIRST token, and
24850 // `Token::Table` fell through to a tail that rejected
24851 // what it got. `parse_table_shorthand` has returned a
24852 // desugared SelectStatement since the shorthand landed —
24853 // only the routing was missing, here and in the derived
24854 // table's second-token gate. Round 868 found both by
24855 // putting the shorthand in a subquery; every earlier
24856 // check had used a top-level form.
24857 Token::Table
24858 if matches!(
24859 self.tokens.get(self.pos + 1),
24860 Some(Token::Ident(_) | Token::QuotedIdent(_))
24861 ) =>
24862 {
24863 let mut head = self.parse_table_shorthand()?;
24864 self.parse_setop_chain_into(&mut head)?;
24865 self.parse_select_tail_into(&mut head)?;
24866 crate::ast::CteBody::Select(head)
24867 }
24868 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24869 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24870 // the shared rows helper onto a Select body.
24871 Token::Values => {
24872 self.advance(); // VALUES
24873 let mut head = self.parse_values_rows_body()?;
24874 // A VALUES seed can head a set-operation chain —
24875 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24876 // SELECT n+1 FROM t …). Attach any trailing
24877 // UNION / INTERSECT / EXCEPT peers so the
24878 // recursive-CTE body parses like the SELECT seed.
24879 self.parse_setop_chain_into(&mut head)?;
24880 crate::ast::CteBody::Select(head)
24881 }
24882 Token::Insert => {
24883 let inner = self.parse_one_statement()?;
24884 let Statement::Insert(s) = inner else {
24885 unreachable!("Token::Insert routes to Insert");
24886 };
24887 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24888 }
24889 _ if is_update_kw => {
24890 let inner = self.parse_one_statement()?;
24891 let Statement::Update(s) = inner else {
24892 return Err(
24893 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24894 );
24895 };
24896 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24897 }
24898 _ if is_delete_kw => {
24899 let inner = self.parse_one_statement()?;
24900 let Statement::Delete(s) = inner else {
24901 return Err(
24902 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24903 );
24904 };
24905 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24906 }
24907 // v7.39 (round 149) — PG 17 allows MERGE as a
24908 // data-modifying CTE body.
24909 _ if is_merge_kw => {
24910 let inner = self.parse_one_statement()?;
24911 let Statement::Merge(s) = inner else {
24912 return Err(
24913 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24914 );
24915 };
24916 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24917 }
24918 // v7.39 (round 151) — a CTE body may itself be
24919 // WITH-headed (PG grammar: PreparableStmt carries its
24920 // own with_clause). The nested statement keeps its own
24921 // ctes; the modifying-CTE-at-top-level rule is enforced
24922 // at execution.
24923 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24924 self.advance(); // WITH
24925 match self.parse_with_cte_then_select()? {
24926 Statement::Select(s) => crate::ast::CteBody::Select(s),
24927 Statement::Insert(s) => {
24928 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24929 }
24930 Statement::Update(s) => {
24931 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24932 }
24933 Statement::Delete(s) => {
24934 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24935 }
24936 Statement::Merge(s) => {
24937 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24938 }
24939
24940 other => {
24941 return Err(self.err(format!(
24942 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24943 )));
24944 }
24945 }
24946 }
24947 other => {
24948 return Err(self.err(format!(
24949 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24950 )));
24951 }
24952 };
24953 if !matches!(self.peek(), Token::RParen) {
24954 return Err(self.err(format!(
24955 "expected ')' after CTE body, got {:?}",
24956 self.peek()
24957 )));
24958 }
24959 self.advance();
24960 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24961 // CTE, desugared into extra body columns by the engine.
24962 let search = self.parse_cte_search_clause()?;
24963 let cycle = self.parse_cte_cycle_clause()?;
24964 let mut cte = crate::ast::Cte {
24965 name,
24966 body,
24967 recursive,
24968 column_overrides,
24969 search,
24970 cycle,
24971 };
24972 self.validate_recursive_cte(&cte)?;
24973 self.desugar_cte_search_cycle(&mut cte)?;
24974 ctes.push(cte);
24975 if matches!(self.peek(), Token::Comma) {
24976 self.advance();
24977 continue;
24978 }
24979 break;
24980 }
24981 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24982 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24983 // the parsed CTEs to whichever statement the body produces.
24984 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24985 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24986 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24987 match self.peek() {
24988 Token::Select => {
24989 let body_stmt = self.parse_select_stmt()?;
24990 let Statement::Select(mut body) = body_stmt else {
24991 unreachable!()
24992 };
24993 body.ctes = ctes;
24994 Ok(Statement::Select(body))
24995 }
24996 Token::Insert => {
24997 let body_stmt = self.parse_one_statement()?;
24998 let Statement::Insert(mut body) = body_stmt else {
24999 unreachable!()
25000 };
25001 body.ctes = ctes;
25002 Ok(Statement::Insert(body))
25003 }
25004 _ if outer_is_update => {
25005 let body_stmt = self.parse_one_statement()?;
25006 let Statement::Update(mut body) = body_stmt else {
25007 return Err(self.err(format!("expected UPDATE after WITH clause")));
25008 };
25009 body.ctes = ctes;
25010 Ok(Statement::Update(body))
25011 }
25012 _ if outer_is_delete => {
25013 let body_stmt = self.parse_one_statement()?;
25014 let Statement::Delete(mut body) = body_stmt else {
25015 return Err(self.err(format!("expected DELETE after WITH clause")));
25016 };
25017 body.ctes = ctes;
25018 Ok(Statement::Delete(body))
25019 }
25020 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
25021 // WITH RECURSIVE is rejected with PG's exact message
25022 // (parse analysis, transformWithClause).
25023 _ if outer_is_merge => {
25024 if recursive {
25025 return Err(self.err(String::from(
25026 "WITH RECURSIVE is not supported for MERGE statement",
25027 )));
25028 }
25029 let body_stmt = self.parse_one_statement()?;
25030 let Statement::Merge(mut body) = body_stmt else {
25031 return Err(self.err(format!("expected MERGE after WITH clause")));
25032 };
25033 body.ctes = ctes;
25034 Ok(Statement::Merge(body))
25035 }
25036 other => Err(self.err(format!(
25037 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
25038 ))),
25039 }
25040 }
25041
25042 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
25043 /// already consumed the leading `EXISTS` ident via
25044 /// `self.advance()`.
25045 /// v7.13.0 — parse the rest of a `CASE … END` expression after
25046 /// the leading `CASE` ident has been consumed (mailrs round-5
25047 /// G9). Supports both the searched form
25048 /// (`CASE WHEN cond THEN val …`) and the simple form
25049 /// (`CASE operand WHEN val THEN val …`).
25050 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
25051 // Disambiguate searched vs simple form: if the next token
25052 // is `WHEN`, we're in the searched form. Otherwise the
25053 // intervening expression is the operand.
25054 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
25055 None
25056 } else {
25057 Some(Box::new(self.parse_expr(0)?))
25058 };
25059 let mut branches: Vec<(Expr, Expr)> = Vec::new();
25060 loop {
25061 match self.peek() {
25062 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
25063 self.advance();
25064 let cond = self.parse_expr(0)?;
25065 match self.peek() {
25066 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
25067 self.advance();
25068 }
25069 other => {
25070 return Err(self.err(alloc::format!(
25071 "expected THEN after CASE WHEN <expr>, got {other:?}"
25072 )));
25073 }
25074 }
25075 let value = self.parse_expr(0)?;
25076 branches.push((cond, value));
25077 }
25078 _ => break,
25079 }
25080 }
25081 if branches.is_empty() {
25082 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
25083 }
25084 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
25085 {
25086 self.advance();
25087 Some(Box::new(self.parse_expr(0)?))
25088 } else {
25089 None
25090 };
25091 match self.peek() {
25092 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
25093 self.advance();
25094 }
25095 other => {
25096 return Err(self.err(alloc::format!(
25097 "expected END to close CASE expression, got {other:?}"
25098 )));
25099 }
25100 }
25101 Ok(Expr::Case {
25102 operand,
25103 branches,
25104 else_branch,
25105 })
25106 }
25107
25108 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
25109 /// query-source position (EXISTS / IN / INSERT source / CTE body /
25110 /// view body). Caller consumed the WITH keyword. Only a SELECT
25111 /// outer is grammatical here; the data-modifying-CTE-at-top-level
25112 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25113 /// maps correctly.
25114 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25115 let inner = self.parse_with_cte_then_select()?;
25116 match inner {
25117 Statement::Select(s) => Ok(s),
25118 other => Err(self.err(format!(
25119 "expected SELECT after WITH in a subquery, got {other:?}"
25120 ))),
25121 }
25122 }
25123
25124 /// True when the next token is the (unquoted) WITH keyword. WITH is
25125 /// reserved in PG, so a bare `with` can never be a column reference
25126 /// in these positions; a quoted `"with"` stays an identifier.
25127 fn peek_is_with_kw(&self) -> bool {
25128 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25129 }
25130
25131 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25132 /// `#[inline(never)]` keeps the large SelectStatement temporaries
25133 /// off parse_expr's recursive frame (the nesting-budget stack
25134 /// cliff — see the round-153 gate regression).
25135 #[inline(never)]
25136 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25137 if self.peek_is_with_kw() {
25138 self.advance();
25139 self.parse_nested_with_select()
25140 } else {
25141 match self.parse_select_stmt()? {
25142 Statement::Select(s) => Ok(s),
25143 other => Err(self.err(alloc::format!(
25144 "expected SELECT inside ANY/ALL, got {other:?}"
25145 ))),
25146 }
25147 }
25148 }
25149
25150 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25151 if !matches!(self.peek(), Token::LParen) {
25152 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25153 }
25154 self.advance();
25155 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25156 let s = if self.peek_is_with_kw() {
25157 self.advance();
25158 self.parse_nested_with_select()?
25159 } else {
25160 let inner = self.parse_select_stmt()?;
25161 let Statement::Select(s) = inner else {
25162 unreachable!("parse_select_stmt returns Select")
25163 };
25164 s
25165 };
25166 if !matches!(self.peek(), Token::RParen) {
25167 return Err(self.err(format!(
25168 "expected ')' after EXISTS-subquery, got {:?}",
25169 self.peek()
25170 )));
25171 }
25172 self.advance();
25173 Ok(Expr::Exists {
25174 subquery: Box::new(s),
25175 negated,
25176 })
25177 }
25178
25179 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25180 self.advance(); // IN
25181 if !matches!(self.peek(), Token::LParen) {
25182 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25183 }
25184 self.advance();
25185 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25186 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25187 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25188 let s = if self.peek_is_with_kw() {
25189 self.advance();
25190 self.parse_nested_with_select()?
25191 } else {
25192 let inner = self.parse_select_stmt()?;
25193 let Statement::Select(s) = inner else {
25194 unreachable!("parse_select_stmt always returns Statement::Select")
25195 };
25196 s
25197 };
25198 if !matches!(self.peek(), Token::RParen) {
25199 return Err(self.err(format!(
25200 "expected ')' after IN-subquery, got {:?}",
25201 self.peek()
25202 )));
25203 }
25204 self.advance();
25205 return Ok(Expr::InSubquery {
25206 expr: Box::new(expr),
25207 subquery: Box::new(s),
25208 negated,
25209 });
25210 }
25211 let mut elements = Vec::new();
25212 if !matches!(self.peek(), Token::RParen) {
25213 loop {
25214 elements.push(self.parse_expr(0)?);
25215 match self.peek() {
25216 Token::Comma => {
25217 self.advance();
25218 }
25219 Token::RParen => break,
25220 other => {
25221 return Err(
25222 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25223 );
25224 }
25225 }
25226 }
25227 }
25228 self.advance(); // ')'
25229 // v7.30.2 (mailrs round-25) — flat InList node instead of a
25230 // left-deep OR-Eq chain: chain depth scaled with the element
25231 // count and overflowed the stack (eval + drop are recursive).
25232 if elements.is_empty() {
25233 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25234 }
25235 Ok(Expr::InList {
25236 expr: Box::new(expr),
25237 list: elements,
25238 negated,
25239 })
25240 }
25241
25242 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25243 /// already consumed by the caller. Elements must be numeric literals
25244 /// (with optional unary `-`); any compound expression is rejected at
25245 /// parse time so the runtime never needs to evaluate inside a vector.
25246 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25247 /// has already consumed the `EXTRACT` token before calling us —
25248 /// we pick up at the opening `(`.
25249 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25250 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25251 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25252 /// per-column OR-fold of
25253 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25254 /// term)` so the existing FTS evaluator handles semantics.
25255 ///
25256 /// The mode modifier is accepted-and-ignored at v7.17 — all
25257 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25258 /// mode operators (`+foo -bar`) would need their own parser
25259 /// (Phase 2.2c); customers who hit them today already get a
25260 /// correct lexeme-match against the bare term, only without
25261 /// the +/- precedence the customer asked for.
25262 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25263 // Already at `MATCH`-consumed position; the dispatcher
25264 // confirmed the next token is `(`.
25265 if !matches!(self.peek(), Token::LParen) {
25266 return Err(self.err(alloc::format!(
25267 "expected '(' after MATCH, got {:?}",
25268 self.peek()
25269 )));
25270 }
25271 self.advance();
25272 let mut cols: Vec<Expr> = Vec::new();
25273 loop {
25274 cols.push(self.parse_expr(0)?);
25275 match self.peek() {
25276 Token::Comma => {
25277 self.advance();
25278 }
25279 Token::RParen => break,
25280 other => {
25281 return Err(self.err(alloc::format!(
25282 "expected ',' or ')' in MATCH column list, got {other:?}"
25283 )));
25284 }
25285 }
25286 }
25287 self.advance(); // ')'
25288 // Expect AGAINST.
25289 match self.peek() {
25290 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25291 self.advance();
25292 }
25293 other => {
25294 return Err(self.err(alloc::format!(
25295 "expected AGAINST after MATCH column list, got {other:?}"
25296 )));
25297 }
25298 }
25299 if !matches!(self.peek(), Token::LParen) {
25300 return Err(self.err(alloc::format!(
25301 "expected '(' after AGAINST, got {:?}",
25302 self.peek()
25303 )));
25304 }
25305 self.advance();
25306 // Read AGAINST's argument as a single primary token —
25307 // string literal, placeholder, or column-ref ident. We
25308 // can't call `parse_expr` / `parse_unary` here because
25309 // the postfix chain inside `parse_atom` would greedily
25310 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25311 // and fail at "expected '(' after IN". Customers always
25312 // write a literal or bound parameter in AGAINST, so this
25313 // restriction is non-blocking; the error path explains
25314 // the limit if a more complex expression shows up.
25315 let term = match self.advance() {
25316 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25317 Token::Placeholder(n) => Expr::Placeholder(n),
25318 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25319 qualifier: None,
25320 name: s,
25321 }),
25322 other => {
25323 return Err(self.err(alloc::format!(
25324 "MATCH ... AGAINST(<term>) expects a string literal, \
25325 bound parameter, or column ref, got {other:?}"
25326 )));
25327 }
25328 };
25329 // Optional mode tail — accept-and-ignore at v7.17:
25330 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25331 // IN BOOLEAN MODE
25332 // WITH QUERY EXPANSION
25333 loop {
25334 match self.peek() {
25335 // IN lexes as a reserved Token::In, not an ident,
25336 // so it gets its own arm.
25337 Token::In => {
25338 self.advance();
25339 }
25340 Token::Ident(s) | Token::QuotedIdent(s)
25341 if s.eq_ignore_ascii_case("natural")
25342 || s.eq_ignore_ascii_case("language")
25343 || s.eq_ignore_ascii_case("boolean")
25344 || s.eq_ignore_ascii_case("mode")
25345 || s.eq_ignore_ascii_case("with")
25346 || s.eq_ignore_ascii_case("query")
25347 || s.eq_ignore_ascii_case("expansion") =>
25348 {
25349 self.advance();
25350 }
25351 _ => break,
25352 }
25353 }
25354 if !matches!(self.peek(), Token::RParen) {
25355 return Err(self.err(alloc::format!(
25356 "expected ')' to close AGAINST, got {:?}",
25357 self.peek()
25358 )));
25359 }
25360 self.advance();
25361 // Build per-column `to_tsvector('simple', col) @@
25362 // plainto_tsquery('simple', term)` and OR-fold.
25363 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25364 let plainto = Expr::FunctionCall {
25365 name: String::from("plainto_tsquery"),
25366 args: alloc::vec![simple_lit(), term.clone()],
25367 };
25368 let mut folded: Option<Expr> = None;
25369 for col in cols {
25370 let to_tsv = Expr::FunctionCall {
25371 name: String::from("to_tsvector"),
25372 args: alloc::vec![simple_lit(), col],
25373 };
25374 let leaf = Expr::Binary {
25375 lhs: Box::new(to_tsv),
25376 op: crate::ast::BinOp::TsMatch,
25377 rhs: Box::new(plainto.clone()),
25378 };
25379 folded = Some(match folded {
25380 None => leaf,
25381 Some(prev) => Expr::Binary {
25382 lhs: Box::new(prev),
25383 op: crate::ast::BinOp::Or,
25384 rhs: Box::new(leaf),
25385 },
25386 });
25387 }
25388 match folded {
25389 Some(e) => Ok(e),
25390 None => Err(self.err(String::from(
25391 "MATCH(...) AGAINST(...) requires at least one column",
25392 ))),
25393 }
25394 }
25395
25396 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25397 if !matches!(self.peek(), Token::LParen) {
25398 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25399 }
25400 self.advance();
25401 let field_name = self.expect_ident_like()?;
25402 let field = match field_name.to_ascii_lowercase().as_str() {
25403 // PG accepts the plural spellings (years/months/…/millenniums) as
25404 // aliases for the singular fields — its datetime unit table has both.
25405 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25406 "year" | "years" => ExtractField::Year,
25407 "month" | "months" => ExtractField::Month,
25408 "day" | "days" => ExtractField::Day,
25409 "hour" | "hours" => ExtractField::Hour,
25410 "minute" | "minutes" => ExtractField::Minute,
25411 "second" | "seconds" => ExtractField::Second,
25412 "microsecond" | "microseconds" => ExtractField::Microsecond,
25413 "epoch" => ExtractField::Epoch,
25414 "dow" => ExtractField::Dow,
25415 "isodow" => ExtractField::Isodow,
25416 "doy" => ExtractField::Doy,
25417 "week" | "weeks" => ExtractField::Week,
25418 "isoyear" => ExtractField::Isoyear,
25419 "quarter" => ExtractField::Quarter,
25420 "decade" | "decades" => ExtractField::Decade,
25421 "century" | "centuries" => ExtractField::Century,
25422 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25423 "julian" => ExtractField::Julian,
25424 "millisecond" | "milliseconds" => ExtractField::Millisecond,
25425 "timezone" => ExtractField::Timezone,
25426 "timezone_hour" => ExtractField::TimezoneHour,
25427 "timezone_minute" => ExtractField::TimezoneMinute,
25428 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25429 // reports an unknown one with the source type (22023); carry the
25430 // raw name so eval can word it.
25431 other => ExtractField::Other(alloc::string::String::from(other)),
25432 };
25433 if !matches!(self.peek(), Token::From) {
25434 return Err(self.err(format!(
25435 "expected FROM after EXTRACT field, got {:?}",
25436 self.peek()
25437 )));
25438 }
25439 self.advance();
25440 let source = self.parse_expr(0)?;
25441 if !matches!(self.peek(), Token::RParen) {
25442 return Err(self.err(format!(
25443 "expected ')' to close EXTRACT, got {:?}",
25444 self.peek()
25445 )));
25446 }
25447 self.advance();
25448 Ok(Expr::Extract {
25449 field,
25450 source: Box::new(source),
25451 })
25452 }
25453
25454 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25455 /// is already consumed; we expect a single string literal next and
25456 /// resolve it into `Literal::Interval` at parse time so the engine
25457 /// never has to re-tokenise inside the string.
25458 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25459 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25460 /// is the SQL-standard form and is left to the path below.
25461 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25462 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25463 let (offset, sign) = match self.peek() {
25464 Token::Minus => (1, "-"),
25465 _ => (0, ""),
25466 };
25467 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25468 return None;
25469 };
25470 self.tokens
25471 .get(self.pos + offset + 1)
25472 .filter(|t| mysql_interval_unit(t).is_some())?;
25473 Some((alloc::format!("{sign}{n}"), offset + 1))
25474 }
25475
25476 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25477 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25478 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25479 ///
25480 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25481 /// this by parsing the group and then restoring `self.pos` — which could
25482 /// never have worked, because `advance()` DESTROYS the token it returns
25483 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25484 /// inert only because both branches errored back then.
25485 fn interval_paren_is_quantity(&self) -> bool {
25486 let mut depth = 0usize;
25487 let mut saw_top_level_comma = false;
25488 let mut i = self.pos;
25489 while let Some(tok) = self.tokens.get(i) {
25490 match tok {
25491 Token::LParen => depth += 1,
25492 Token::RParen => {
25493 depth = depth.saturating_sub(1);
25494 if depth == 0 {
25495 return !saw_top_level_comma
25496 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25497 .is_some();
25498 }
25499 }
25500 // A comma directly inside the outermost parens means the
25501 // argument list of the INTERVAL() function.
25502 Token::Comma if depth == 1 => saw_top_level_comma = true,
25503 Token::Eof => return false,
25504 _ => {}
25505 }
25506 i += 1;
25507 }
25508 false
25509 }
25510
25511 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25512 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25513 // (the index of the last Ni ≤ N), distinct from the interval literal.
25514 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25515 // is decided by a non-destructive lookahead (round 422) before either
25516 // branch consumes anything. MySQL only.
25517 if self.mysql_dialect
25518 && matches!(self.peek(), Token::LParen)
25519 && !self.interval_paren_is_quantity()
25520 {
25521 self.advance(); // (
25522 let mut args = Vec::new();
25523 if !matches!(self.peek(), Token::RParen) {
25524 loop {
25525 args.push(self.parse_expr(0)?);
25526 if matches!(self.peek(), Token::Comma) {
25527 self.advance();
25528 continue;
25529 }
25530 break;
25531 }
25532 }
25533 if !matches!(self.peek(), Token::RParen) {
25534 return Err(self.err(alloc::format!(
25535 "expected ')' after INTERVAL() arguments, got {:?}",
25536 self.peek()
25537 )));
25538 }
25539 self.advance(); // )
25540 return Ok(Expr::FunctionCall {
25541 name: alloc::string::String::from("interval"),
25542 args,
25543 });
25544 }
25545 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25546 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25547 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25548 // writes every date arithmetic there is, and it did not parse at
25549 // all. PG rejects the unquoted form outright (`syntax error at or
25550 // near "1"`, measured), so it is taken only in the MySQL dialect —
25551 // PG's own `INTERVAL '1' DAY` is untouched below.
25552 if self.mysql_dialect
25553 && let Some((text, consume)) = self.peek_unquoted_interval_count()
25554 {
25555 for _ in 0..consume {
25556 self.advance(); // the optional `-` and the number
25557 }
25558 let Some(unit) = mysql_interval_unit(self.peek()) else {
25559 return Err(self.err(alloc::format!(
25560 "expected an interval unit after INTERVAL {text}, got {:?}",
25561 self.peek()
25562 )));
25563 };
25564 self.advance(); // the unit
25565 let (months, days, micros) = scale_mysql_interval(&text, unit)
25566 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25567 return Ok(Expr::Literal(Literal::Interval {
25568 months,
25569 days,
25570 micros,
25571 // The canonical rendering, so Display round-trips into a
25572 // form both dialects read back.
25573 text: alloc::format!("{text} {unit}"),
25574 }));
25575 }
25576 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25577 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25578 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25579 // Those cannot fold into a compile-time `Literal::Interval`, so they
25580 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25581 // builtin, which builds the value at run time (and yields NULL for a
25582 // NULL quantity, as MariaDB does). The literal path above still folds
25583 // the constant case — it is cheaper and round-trips through Display.
25584 //
25585 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25586 // MySQL's quoted spelling) keep the qualifier path below.
25587 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25588 let qty = self.parse_expr(0)?;
25589 let Some(unit) = mysql_interval_unit(self.peek()) else {
25590 return Err(self.err(alloc::format!(
25591 "expected an interval unit after INTERVAL <expr>, got {:?}",
25592 self.peek()
25593 )));
25594 };
25595 self.advance(); // the unit
25596 return Ok(make_interval_call(qty, unit));
25597 }
25598 let tok = self.advance();
25599 let Token::String(text) = tok else {
25600 return Err(self.err(format!(
25601 "expected string literal after INTERVAL, got {tok:?}"
25602 )));
25603 };
25604 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25605 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25606 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25607 // bare number means and the leading/trailing precision.
25608 let field1 = interval_field_of(self.peek());
25609 let qualifier = if let Some(f1) = field1 {
25610 self.advance();
25611 let f2 = if matches!(self.peek(), Token::To) {
25612 self.advance();
25613 let Some(f) = interval_field_of(self.peek()) else {
25614 return Err(self.err(format!(
25615 "expected an interval field after TO, got {:?}",
25616 self.peek()
25617 )));
25618 };
25619 self.advance();
25620 Some(f)
25621 } else {
25622 None
25623 };
25624 Some((f1, f2))
25625 } else {
25626 None
25627 };
25628 let (months, days, micros) = match qualifier {
25629 Some(q) => interpret_qualified_interval(&text, q),
25630 None => parse_interval_text(&text),
25631 }
25632 .ok_or_else(|| ParseError {
25633 message: format!(
25634 "cannot parse INTERVAL {text:?}; \
25635 expected `<n> <unit> [<n> <unit> ...]` with units \
25636 microsecond[s], millisecond[s], second[s], minute[s], \
25637 hour[s], day[s], week[s], month[s], year[s]"
25638 ),
25639 token_pos: self.consumed_pos(),
25640 })?;
25641 Ok(Expr::Literal(Literal::Interval {
25642 months,
25643 days,
25644 micros,
25645 text,
25646 }))
25647 }
25648
25649 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25650 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25651 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25652 /// than a pgvector literal.
25653 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25654 self.advance(); // consume `[`
25655 let mut items: Vec<Expr> = Vec::new();
25656 if !matches!(self.peek(), Token::RBracket) {
25657 loop {
25658 if matches!(self.peek(), Token::LBracket) {
25659 items.push(self.parse_array_bracket_body()?);
25660 } else {
25661 items.push(self.parse_expr(0)?);
25662 }
25663 match self.peek() {
25664 Token::Comma => {
25665 self.advance();
25666 }
25667 Token::RBracket => break,
25668 other => {
25669 return Err(self.err(alloc::format!(
25670 "expected ',' or ']' in array literal, got {other:?}"
25671 )));
25672 }
25673 }
25674 }
25675 }
25676 self.advance(); // consume `]`
25677 Ok(Expr::Array(items))
25678 }
25679
25680 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25681 let mut elems = Vec::new();
25682 if matches!(self.peek(), Token::RBracket) {
25683 self.advance();
25684 return Ok(Expr::Literal(Literal::Vector(elems)));
25685 }
25686 loop {
25687 let e = self.parse_expr(0)?;
25688 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25689 message: format!("vector element must be a numeric literal, got {e:?}"),
25690 token_pos: self.pos,
25691 })?;
25692 elems.push(x);
25693 match self.peek() {
25694 Token::Comma => {
25695 self.advance();
25696 }
25697 Token::RBracket => {
25698 self.advance();
25699 break;
25700 }
25701 other => {
25702 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25703 }
25704 }
25705 }
25706 Ok(Expr::Literal(Literal::Vector(elems)))
25707 }
25708
25709 /// Atom that started with an identifier: could be `t.col`, `col`, or
25710 /// `func(arg, ...)`. Detect each shape by looking at the next token.
25711 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25712 /// [, ...])`. Caller has already consumed `OVER`. Either clause
25713 /// is optional; an empty `()` is also legal (PG semantics).
25714 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25715 /// modifier between `name(args)` and `OVER (...)`. Default is
25716 /// `Respect`. Unrecognised idents leave the stream unchanged.
25717 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25718 let Token::Ident(s) = self.peek().clone() else {
25719 return NullTreatment::Respect;
25720 };
25721 let is_ignore = s.eq_ignore_ascii_case("ignore");
25722 let is_respect = s.eq_ignore_ascii_case("respect");
25723 if !is_ignore && !is_respect {
25724 return NullTreatment::Respect;
25725 }
25726 // Lookahead for NULLS — only consume both tokens together.
25727 // pos+1 must hold a "nulls" ident.
25728 if self.pos + 1 < self.tokens.len()
25729 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25730 && s2.eq_ignore_ascii_case("nulls")
25731 {
25732 self.advance();
25733 self.advance();
25734 return if is_ignore {
25735 NullTreatment::Ignore
25736 } else {
25737 NullTreatment::Respect
25738 };
25739 }
25740 NullTreatment::Respect
25741 }
25742
25743 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25744 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25745 /// (same shape as the `OVER` tail). Consumes the whole clause and
25746 /// returns the predicate; returns `None` when no `FILTER` follows.
25747 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25748 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25749 return Ok(None);
25750 };
25751 if !s.eq_ignore_ascii_case("filter") {
25752 return Ok(None);
25753 }
25754 self.advance(); // FILTER
25755 if !matches!(self.peek(), Token::LParen) {
25756 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25757 }
25758 self.advance(); // (
25759 if !matches!(self.peek(), Token::Where) {
25760 return Err(self.err(format!(
25761 "expected WHERE inside FILTER (...), got {:?}",
25762 self.peek()
25763 )));
25764 }
25765 self.advance(); // WHERE
25766 let cond = self.parse_expr(0)?;
25767 if !matches!(self.peek(), Token::RParen) {
25768 return Err(self.err(format!(
25769 "expected ')' to close FILTER (WHERE ...), got {:?}",
25770 self.peek()
25771 )));
25772 }
25773 self.advance(); // )
25774 Ok(Some(Box::new(cond)))
25775 }
25776
25777 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25778 /// the separator as the aggregate's second argument, which is the
25779 /// shape `string_agg` already takes. Returns whether one was there.
25780 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25781 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25782 return Ok(false);
25783 }
25784 self.advance();
25785 let Token::String(sep) = self.peek().clone() else {
25786 return Err(self.err(alloc::format!(
25787 "expected a string literal after SEPARATOR, got {:?}",
25788 self.peek()
25789 )));
25790 };
25791 self.advance();
25792 args.push(Expr::Literal(Literal::String(sep)));
25793 Ok(true)
25794 }
25795
25796 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25797 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25798 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25799 /// keys, or an empty vec when no `WITHIN GROUP` follows.
25800 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25801 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25802 return Ok(Vec::new());
25803 };
25804 if !s.eq_ignore_ascii_case("within") {
25805 return Ok(Vec::new());
25806 }
25807 self.advance(); // WITHIN
25808 if !matches!(self.peek(), Token::Group) {
25809 return Err(self.err(format!(
25810 "expected GROUP after WITHIN, got {:?}",
25811 self.peek()
25812 )));
25813 }
25814 self.advance(); // GROUP
25815 if !matches!(self.peek(), Token::LParen) {
25816 return Err(self.err(format!(
25817 "expected '(' after WITHIN GROUP, got {:?}",
25818 self.peek()
25819 )));
25820 }
25821 self.advance(); // (
25822 if !matches!(self.peek(), Token::Order) {
25823 return Err(self.err(format!(
25824 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25825 self.peek()
25826 )));
25827 }
25828 self.advance(); // ORDER
25829 if !self.peek_is_by() {
25830 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25831 }
25832 self.advance(); // BY
25833 let mut keys: Vec<OrderBy> = Vec::new();
25834 loop {
25835 // v7.39 (round 691) — save/restore, the discipline this parser
25836 // already uses around `pending_sample_preds`, so a subquery inside
25837 // a key neither inherits nor leaks the channel.
25838 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25839 let saved_coll = self.order_key_collation.take();
25840 let parsed = self.parse_expr(0);
25841 self.in_order_by_key = saved_flag;
25842 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25843 let expr = parsed?;
25844 let desc = if matches!(self.peek(), Token::Desc) {
25845 self.advance();
25846 true
25847 } else if matches!(self.peek(), Token::Asc) {
25848 self.advance();
25849 false
25850 } else {
25851 false
25852 };
25853 let nulls_first = self.parse_optional_nulls_placement()?;
25854 keys.push(OrderBy {
25855 expr,
25856 desc,
25857 nulls_first,
25858 collation,
25859 });
25860 if matches!(self.peek(), Token::Comma) {
25861 self.advance();
25862 } else {
25863 break;
25864 }
25865 }
25866 if !matches!(self.peek(), Token::RParen) {
25867 return Err(self.err(format!(
25868 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25869 self.peek()
25870 )));
25871 }
25872 self.advance(); // )
25873 Ok(keys)
25874 }
25875
25876 /// No frame clause is supported.
25877 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25878 fn parse_over_clause(
25879 &mut self,
25880 ) -> Result<
25881 (
25882 Vec<Expr>,
25883 Vec<(Expr, bool, Option<bool>)>,
25884 Option<WindowFrame>,
25885 ),
25886 ParseError,
25887 > {
25888 // `OVER w` — a named-window reference. The WINDOW clause
25889 // parses after the select list, so the name rides out as a
25890 // marker in partition_by; parse_bare_select substitutes the
25891 // definition once the clause is known.
25892 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25893 let name = w.clone();
25894 self.advance();
25895 return Ok((
25896 alloc::vec![Expr::Column(crate::ast::ColumnName {
25897 qualifier: Some("__named_window__".to_string()),
25898 name,
25899 })],
25900 Vec::new(),
25901 None,
25902 ));
25903 }
25904 if !matches!(self.peek(), Token::LParen) {
25905 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25906 }
25907 self.advance();
25908 let mut partition_by = Vec::new();
25909 let mut order_by = Vec::new();
25910 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25911 // window, refined in place. PG's rules (probed against 18.4) differ
25912 // from the bare `OVER w1` form, so the reference rides out under its
25913 // own marker and `substitute_named_windows` applies them. The base
25914 // name is any leading identifier that isn't a window-spec keyword.
25915 let base_window = match self.peek() {
25916 Token::Ident(s) | Token::QuotedIdent(s)
25917 if !s.eq_ignore_ascii_case("partition")
25918 && !s.eq_ignore_ascii_case("rows")
25919 && !s.eq_ignore_ascii_case("range")
25920 && !s.eq_ignore_ascii_case("groups") =>
25921 {
25922 let n = s.clone();
25923 self.advance();
25924 Some(n)
25925 }
25926 _ => None,
25927 };
25928 // PARTITION BY ?
25929 // v7.37.6-B promoted PARTITION to a reserved keyword
25930 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25931 // `Token::Ident("partition")`. Accept both so older sources
25932 // and the new lexer surface land on the same path.
25933 let is_partition_kw = match self.peek() {
25934 Token::Partition => true,
25935 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25936 _ => false,
25937 };
25938 if is_partition_kw {
25939 self.advance();
25940 if !self.peek_is_by() {
25941 return Err(self.err(format!(
25942 "expected BY after PARTITION, got {:?}",
25943 self.peek()
25944 )));
25945 }
25946 self.advance();
25947 loop {
25948 partition_by.push(self.parse_expr(0)?);
25949 if matches!(self.peek(), Token::Comma) {
25950 self.advance();
25951 continue;
25952 }
25953 break;
25954 }
25955 }
25956 // ORDER BY ?
25957 if matches!(self.peek(), Token::Order) {
25958 self.advance();
25959 if !self.peek_is_by() {
25960 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25961 }
25962 self.advance();
25963 loop {
25964 let e = self.parse_expr(0)?;
25965 let desc = if matches!(self.peek(), Token::Desc) {
25966 self.advance();
25967 true
25968 } else if matches!(self.peek(), Token::Asc) {
25969 self.advance();
25970 false
25971 } else {
25972 false
25973 };
25974 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25975 let nulls_first = self.parse_optional_nulls_placement()?;
25976 order_by.push((e, desc, nulls_first));
25977 if matches!(self.peek(), Token::Comma) {
25978 self.advance();
25979 continue;
25980 }
25981 break;
25982 }
25983 }
25984 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25985 // Both keywords come through the lexer as identifiers; match
25986 // case-insensitively.
25987 let mut frame: Option<WindowFrame> = None;
25988 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25989 let kind = if s.eq_ignore_ascii_case("rows") {
25990 Some(FrameKind::Rows)
25991 } else if s.eq_ignore_ascii_case("range") {
25992 Some(FrameKind::Range)
25993 } else if s.eq_ignore_ascii_case("groups") {
25994 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25995 Some(FrameKind::Groups)
25996 } else {
25997 None
25998 };
25999 if let Some(kind) = kind {
26000 self.advance();
26001 frame = Some(self.parse_frame_tail(kind)?);
26002 }
26003 }
26004 if !matches!(self.peek(), Token::RParen) {
26005 return Err(self.err(format!(
26006 "expected ')' to close OVER clause, got {:?}",
26007 self.peek()
26008 )));
26009 }
26010 self.advance();
26011 if let Some(base) = base_window {
26012 // A copy may refine but never override the base's partitioning
26013 // (PG rejects it outright, before looking the name up).
26014 if !partition_by.is_empty() {
26015 return Err(self.err(alloc::format!(
26016 "cannot override PARTITION BY clause of window \"{base}\""
26017 )));
26018 }
26019 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
26020 qualifier: Some("__named_window_ref__".to_string()),
26021 name: base,
26022 })];
26023 }
26024 Ok((partition_by, order_by, frame))
26025 }
26026
26027 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
26028 /// or `RANGE` keyword was just consumed. Accepts both
26029 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
26030 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
26031 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
26032 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
26033 let (start, end) = if matches!(self.peek(), Token::Between) {
26034 self.advance();
26035 let start = self.parse_frame_bound()?;
26036 if !matches!(self.peek(), Token::And) {
26037 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
26038 }
26039 self.advance();
26040 let end = self.parse_frame_bound()?;
26041 (start, Some(end))
26042 } else {
26043 (self.parse_frame_bound()?, None)
26044 };
26045 let exclude = self.parse_frame_exclusion()?;
26046 Ok(WindowFrame {
26047 kind,
26048 start,
26049 end,
26050 exclude,
26051 })
26052 }
26053
26054 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
26055 /// after a frame spec. NO OTHERS is the default no-op.
26056 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
26057 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
26058 return Ok(FrameExclusion::NoOthers);
26059 }
26060 self.advance(); // EXCLUDE
26061 match self.peek() {
26062 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
26063 self.advance();
26064 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
26065 return Err(self.err(format!(
26066 "expected ROW after EXCLUDE CURRENT, got {:?}",
26067 self.peek()
26068 )));
26069 }
26070 self.advance();
26071 Ok(FrameExclusion::CurrentRow)
26072 }
26073 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
26074 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
26075 // Without this arm it fell to the catch-all, whose message
26076 // self-contradictingly listed GROUP as expected.
26077 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
26078 self.advance();
26079 Ok(FrameExclusion::Group)
26080 }
26081 Token::Group => {
26082 self.advance();
26083 Ok(FrameExclusion::Group)
26084 }
26085 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
26086 self.advance();
26087 Ok(FrameExclusion::Ties)
26088 }
26089 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
26090 self.advance();
26091 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
26092 return Err(self.err(format!(
26093 "expected OTHERS after EXCLUDE NO, got {:?}",
26094 self.peek()
26095 )));
26096 }
26097 self.advance();
26098 Ok(FrameExclusion::NoOthers)
26099 }
26100 other => Err(self.err(format!(
26101 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
26102 ))),
26103 }
26104 }
26105
26106 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
26107 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
26108 /// `UNBOUNDED FOLLOWING`.
26109 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26110 // Interval-typed offset for a value-based RANGE frame over a
26111 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26112 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26113 // PRECEDING`.
26114 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26115 let dir = self.expect_ident_like()?;
26116 return if dir.eq_ignore_ascii_case("preceding") {
26117 Ok(FrameBound::IntervalPreceding {
26118 months,
26119 days,
26120 micros,
26121 })
26122 } else if dir.eq_ignore_ascii_case("following") {
26123 Ok(FrameBound::IntervalFollowing {
26124 months,
26125 days,
26126 micros,
26127 })
26128 } else {
26129 Err(self.err(format!(
26130 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26131 )))
26132 };
26133 }
26134 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26135 if let Token::Integer(n) = *self.peek() {
26136 self.advance();
26137 let n: u64 = u64::try_from(n).map_err(|_| {
26138 self.err(format!(
26139 "invalid frame offset {n} — expected non-negative integer"
26140 ))
26141 })?;
26142 let dir = self.expect_ident_like()?;
26143 return if dir.eq_ignore_ascii_case("preceding") {
26144 Ok(FrameBound::OffsetPreceding(n))
26145 } else if dir.eq_ignore_ascii_case("following") {
26146 Ok(FrameBound::OffsetFollowing(n))
26147 } else {
26148 Err(self.err(format!(
26149 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26150 )))
26151 };
26152 }
26153 let first = self.expect_ident_like()?;
26154 if first.eq_ignore_ascii_case("unbounded") {
26155 let dir = self.expect_ident_like()?;
26156 return if dir.eq_ignore_ascii_case("preceding") {
26157 Ok(FrameBound::UnboundedPreceding)
26158 } else if dir.eq_ignore_ascii_case("following") {
26159 Ok(FrameBound::UnboundedFollowing)
26160 } else {
26161 Err(self.err(format!(
26162 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26163 )))
26164 };
26165 }
26166 if first.eq_ignore_ascii_case("current") {
26167 let row = self.expect_ident_like()?;
26168 if !row.eq_ignore_ascii_case("row") {
26169 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26170 }
26171 return Ok(FrameBound::CurrentRow);
26172 }
26173 Err(self.err(format!(
26174 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26175 )))
26176 }
26177
26178 /// Detect and consume a leading interval offset in a frame bound —
26179 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26180 /// `(months, days, micros)`. Leaves the cursor on the trailing
26181 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26182 /// when the next tokens are not an interval offset.
26183 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26184 // Shape A — `INTERVAL '1 day'`.
26185 if matches!(self.peek(), Token::Interval) {
26186 self.advance(); // INTERVAL
26187 let atom = self.parse_interval_atom()?;
26188 if let Expr::Literal(Literal::Interval {
26189 months,
26190 days,
26191 micros,
26192 ..
26193 }) = atom
26194 {
26195 return Ok(Some((months, days, micros)));
26196 }
26197 return Err(self.err("expected an interval literal in frame offset".to_string()));
26198 }
26199 // Shape B — `'1 day'::interval`. Look ahead for the exact
26200 // string / `::` / interval-target triple before committing.
26201 if let Token::String(text) = self.peek() {
26202 let target_is_interval = match self.tokens.get(self.pos + 2) {
26203 Some(Token::Interval) => true,
26204 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26205 _ => false,
26206 };
26207 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26208 && target_is_interval;
26209 if is_cast {
26210 let text = text.clone();
26211 self.advance(); // string
26212 self.advance(); // ::
26213 self.advance(); // interval
26214 let parts = parse_interval_text(&text).ok_or_else(|| {
26215 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26216 })?;
26217 return Ok(Some(parts));
26218 }
26219 }
26220 Ok(None)
26221 }
26222
26223 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26224 // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26225 // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26226 // and all three answer the literal on MySQL 9.7.2.
26227 //
26228 // It is not only syntax, which is why it waited for
26229 // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26230 // because `_binary` makes the comparison byte-wise, while
26231 // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26232 // dropping the charset would have turned a hard error into a
26233 // silently wrong comparison — worse than the error it replaced.
26234 //
26235 // An UNKNOWN charset is NOT an introducer: MySQL answers
26236 // `Unknown column '_nosuch'`, because it parses as a column
26237 // reference followed by a string. So the table decides, and it
26238 // is the same table `SET NAMES` reads.
26239 //
26240 // A space is allowed between the two (`_utf8mb4 'x'`), which
26241 // falls out of asking the token stream rather than the bytes.
26242 if self.mysql_dialect
26243 && let Token::String(_) = self.peek()
26244 {
26245 let lower = first.to_ascii_lowercase();
26246 let charset = if lower == "n" {
26247 // `N'…'` is the national character set, which MySQL
26248 // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26249 //
26250 // utf8mb3 and utf8mb4 both fold case in their default
26251 // collations, so nothing SPG can be asked distinguishes
26252 // the two here: an ablation swapping this to utf8mb4
26253 // reddens no pin. Recorded rather than implied — the
26254 // spelling follows MySQL's documentation, not a
26255 // measurement.
26256 Some("utf8mb3")
26257 } else {
26258 // No filter here: the lookup below IS the test for
26259 // "is this a charset". An ablation that removed a filter
26260 // in this spot reddened nothing, which is how the two
26261 // were found to be one check written twice.
26262 lower.strip_prefix('_')
26263 };
26264 if let Some(cs) = charset
26265 && let Some(collation) = crate::charset::charset_default_collation(cs)
26266 {
26267 let Token::String(body) = self.advance() else {
26268 unreachable!("peeked a string");
26269 };
26270 return Ok(Expr::Collate {
26271 expr: Box::new(Expr::Literal(Literal::String(body))),
26272 collation: String::from(collation),
26273 });
26274 }
26275 }
26276 if matches!(self.peek(), Token::Dot) {
26277 self.advance();
26278 let name = self.expect_ident_like()?;
26279 // v7.14.0 — schema-qualified function call
26280 // `<schema>.<fn>(args)`. PG dumps emit
26281 // `pg_catalog.set_config(...)` in the preamble. SPG
26282 // is single-namespace: drop the schema prefix and
26283 // route the dispatch on the bare function name.
26284 if matches!(self.peek(), Token::LParen) {
26285 return self.finish_ident_atom(name);
26286 }
26287 return Ok(Expr::Column(ColumnName {
26288 qualifier: Some(first),
26289 name,
26290 }));
26291 }
26292 if matches!(self.peek(), Token::LParen) {
26293 self.advance();
26294 // `COUNT(*)` — special-cased here because `*` isn't a normal
26295 // expression token. Lower-case match on `first` since the lexer
26296 // folds identifiers.
26297 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26298 self.advance();
26299 if !matches!(self.peek(), Token::RParen) {
26300 return Err(self.err(format!(
26301 "expected ')' after COUNT(*), got {:?}",
26302 self.peek()
26303 )));
26304 }
26305 self.advance();
26306 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26307 let filter = self.parse_filter_clause()?;
26308 // v4.12: COUNT(*) OVER (...) — same window tail.
26309 let null_treatment = self.parse_null_treatment_modifier();
26310 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26311 && s.eq_ignore_ascii_case("over")
26312 {
26313 self.advance();
26314 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26315 return Ok(Expr::WindowFunction {
26316 name: "count_star".into(),
26317 args: Vec::new(),
26318 partition_by,
26319 order_by,
26320 frame,
26321 null_treatment,
26322 filter,
26323 });
26324 }
26325 if let Some(filter) = filter {
26326 return Ok(Expr::AggregateOrdered {
26327 call: Box::new(Expr::FunctionCall {
26328 name: "count_star".into(),
26329 args: Vec::new(),
26330 }),
26331 order_by: Vec::new(),
26332 distinct: false,
26333 filter: Some(filter),
26334 });
26335 }
26336 return Ok(Expr::FunctionCall {
26337 name: "count_star".into(),
26338 args: Vec::new(),
26339 });
26340 }
26341 // Function call. PG-style: zero-or-more comma-separated args.
26342 let mut args = Vec::new();
26343 // v7.38 (read01, T14) — named-argument notation `argname => value`.
26344 // Names are collected in lock-step with `args` and resolved to
26345 // positional order after the loop (the AST stays positional).
26346 let mut arg_names: Vec<Option<String>> = Vec::new();
26347 let mut agg_order_by: Vec<OrderBy> = Vec::new();
26348 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26349 // seen, so the value arguments before it can be folded.
26350 let mut saw_separator = false;
26351 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26352 // v7.32 (round-29) — accept the dual `ALL` quantifier too
26353 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26354 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26355 self.advance();
26356 true
26357 } else if matches!(self.peek(), Token::All) {
26358 self.advance();
26359 false
26360 } else {
26361 false
26362 };
26363 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26364 // TIMESTAMPDIFF take a bare unit keyword as the first
26365 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26366 // bare type keyword (DATE / TIME / DATETIME); lower them
26367 // onto string literals so the evaluator sees plain text.
26368 if ((first.eq_ignore_ascii_case("timestampadd")
26369 || first.eq_ignore_ascii_case("timestampdiff"))
26370 && matches!(self.peek(), Token::Ident(u) if matches!(
26371 u.to_ascii_lowercase().as_str(),
26372 "microsecond" | "second" | "minute" | "hour" | "day"
26373 | "week" | "month" | "quarter" | "year"
26374 )))
26375 || (first.eq_ignore_ascii_case("get_format")
26376 && matches!(self.peek(), Token::Ident(u) if matches!(
26377 u.to_ascii_lowercase().as_str(),
26378 "date" | "time" | "datetime" | "timestamp"
26379 )))
26380 {
26381 if let Token::Ident(u) = self.peek() {
26382 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26383 }
26384 self.advance();
26385 if matches!(self.peek(), Token::Comma) {
26386 self.advance();
26387 }
26388 }
26389 // `ROW(a, b, …)` keyword constructor. Followed by a
26390 // comparison operator or [NOT] IN it joins the paren
26391 // row-constructor machinery (fieldwise parse-time
26392 // expansion); bare, it stays a `row` call the evaluator
26393 // renders as PG record text.
26394 if first.eq_ignore_ascii_case("row") {
26395 let mut row_items = Vec::new();
26396 if !matches!(self.peek(), Token::RParen) {
26397 loop {
26398 row_items.push(self.parse_expr(0)?);
26399 match self.peek() {
26400 Token::Comma => {
26401 self.advance();
26402 }
26403 Token::RParen => break,
26404 other => {
26405 return Err(self.err(format!(
26406 "expected ',' or ')' in ROW(...), got {other:?}"
26407 )));
26408 }
26409 }
26410 }
26411 }
26412 self.advance(); // ')'
26413 let comparison_follows = matches!(
26414 self.peek(),
26415 Token::Eq
26416 | Token::NotEq
26417 | Token::Lt
26418 | Token::LtEq
26419 | Token::Gt
26420 | Token::GtEq
26421 | Token::In
26422 ) || (matches!(self.peek(), Token::Not)
26423 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26424 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26425 if comparison_follows && !row_items.is_empty() {
26426 return self.parse_row_comparison_tail(row_items);
26427 }
26428 return Ok(Expr::FunctionCall {
26429 name: String::from("row"),
26430 args: row_items,
26431 });
26432 }
26433 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26434 // the parse-mode keyword introduces the source text. SPG
26435 // carries XML as text, so both modes lower to __xmlparse(expr)
26436 // which validates well-formedness and returns Value::Xml.
26437 if first.eq_ignore_ascii_case("xmlparse")
26438 && matches!(self.peek(), Token::Ident(kw)
26439 if kw.eq_ignore_ascii_case("document")
26440 || kw.eq_ignore_ascii_case("content"))
26441 {
26442 let mode = match self.advance() {
26443 Token::Ident(kw) => kw.to_ascii_lowercase(),
26444 _ => unreachable!("peeked an ident"),
26445 };
26446 let src = self.parse_expr(0)?;
26447 if !matches!(self.peek(), Token::RParen) {
26448 return Err(self.err(format!(
26449 "expected ')' to close XMLPARSE, got {:?}",
26450 self.peek()
26451 )));
26452 }
26453 self.advance();
26454 return Ok(Expr::FunctionCall {
26455 name: String::from("__xmlparse"),
26456 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26457 });
26458 }
26459 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26460 // keyword introduces the element name (a bare or quoted
26461 // identifier), then optional content expressions. Lower to a
26462 // plain `xmlelement(name_text, content …)` call.
26463 if first.eq_ignore_ascii_case("xmlelement")
26464 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26465 {
26466 self.advance(); // consume NAME
26467 let elem_name = match self.peek().clone() {
26468 Token::Ident(n) | Token::QuotedIdent(n) => {
26469 self.advance();
26470 n
26471 }
26472 other => {
26473 return Err(self.err(format!(
26474 "expected element name after XMLELEMENT NAME, got {other:?}"
26475 )));
26476 }
26477 };
26478 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26479 while matches!(self.peek(), Token::Comma) {
26480 self.advance();
26481 args.push(self.parse_expr(0)?);
26482 }
26483 if !matches!(self.peek(), Token::RParen) {
26484 return Err(self.err(format!(
26485 "expected ')' to close XMLELEMENT, got {:?}",
26486 self.peek()
26487 )));
26488 }
26489 self.advance();
26490 return Ok(Expr::FunctionCall {
26491 name: String::from("xmlelement"),
26492 args,
26493 });
26494 }
26495 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26496 // becomes a `<name>value</name>` element; a bare column infers its
26497 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26498 // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26499 // `CONVERT(expr USING cs)` was a syntax error at USING, and
26500 // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26501 // `convert(bytea, src, dest)` and answered `column "char" does
26502 // not exist`. Both are casts in MySQL: measured on 9.7.2,
26503 // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26504 // both 'A', and `CONVERT(123, CHAR)` is '123'.
26505 //
26506 // The charset is checked against the same table the introducers
26507 // use, so an unknown one is refused rather than quietly ignored.
26508 if self.mysql_dialect
26509 && first.eq_ignore_ascii_case("convert")
26510 && !matches!(self.peek(), Token::RParen)
26511 {
26512 let save = self.pos;
26513 let inner = self.parse_expr(0)?;
26514 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26515 self.advance();
26516 let cs = match self.peek().clone() {
26517 Token::Ident(n) | Token::QuotedIdent(n) => {
26518 self.advance();
26519 n
26520 }
26521 other => {
26522 return Err(self.err(alloc::format!(
26523 "expected a charset after USING, got {other:?}"
26524 )));
26525 }
26526 };
26527 let lc = cs.to_ascii_lowercase();
26528 if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26529 return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26530 }
26531 if !matches!(self.peek(), Token::RParen) {
26532 return Err(self.err(alloc::format!(
26533 "expected ')' after CONVERT … USING, got {:?}",
26534 self.peek()
26535 )));
26536 }
26537 self.advance();
26538 let target = if lc == "binary" {
26539 CastTarget::Named("binary".to_string())
26540 } else {
26541 CastTarget::Text
26542 };
26543 return self.finish_postfix_casts(Expr::Cast {
26544 expr: alloc::boxed::Box::new(inner),
26545 target,
26546 });
26547 }
26548 if matches!(self.peek(), Token::Comma) {
26549 self.advance();
26550 // A type name here is MySQL's cast form; anything else
26551 // (three string arguments) is PostgreSQL's `convert`,
26552 // which keeps its own path.
26553 if let Ok(target) = self.parse_cast_target()
26554 && matches!(self.peek(), Token::RParen)
26555 {
26556 self.advance();
26557 return self.finish_postfix_casts(Expr::Cast {
26558 expr: alloc::boxed::Box::new(inner),
26559 target,
26560 });
26561 }
26562 }
26563 self.pos = save;
26564 }
26565 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26566 let mut args: Vec<Expr> = Vec::new();
26567 loop {
26568 let val = self.parse_expr(0)?;
26569 let name = if matches!(self.peek(), Token::As) {
26570 self.advance();
26571 match self.peek().clone() {
26572 Token::Ident(n) | Token::QuotedIdent(n) => {
26573 self.advance();
26574 n
26575 }
26576 other => {
26577 return Err(self.err(format!(
26578 "expected name after AS in XMLFOREST, got {other:?}"
26579 )));
26580 }
26581 }
26582 } else if let Expr::Column(c) = &val {
26583 c.name.clone()
26584 } else {
26585 return Err(
26586 self.err("XMLFOREST element without a column name needs AS".into())
26587 );
26588 };
26589 args.push(Expr::Literal(Literal::String(name)));
26590 args.push(val);
26591 if matches!(self.peek(), Token::Comma) {
26592 self.advance();
26593 } else {
26594 break;
26595 }
26596 }
26597 if !matches!(self.peek(), Token::RParen) {
26598 return Err(self.err(format!(
26599 "expected ')' to close XMLFOREST, got {:?}",
26600 self.peek()
26601 )));
26602 }
26603 self.advance();
26604 return Ok(Expr::FunctionCall {
26605 name: String::from("xmlforest"),
26606 args,
26607 });
26608 }
26609 // SQL-standard `POSITION(sub IN str)` — lowers onto
26610 // strpos(str, sub). IN is the argument separator here,
26611 // so the needle parses with the IN-tail suppressed.
26612 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26613 let saved = self.suppress_in_tail;
26614 self.suppress_in_tail = true;
26615 let needle = self.parse_expr(0);
26616 self.suppress_in_tail = saved;
26617 let needle = needle?;
26618 if matches!(self.peek(), Token::In) {
26619 self.advance();
26620 let haystack = self.parse_expr(0)?;
26621 if !matches!(self.peek(), Token::RParen) {
26622 return Err(self.err(format!(
26623 "expected ')' to close POSITION, got {:?}",
26624 self.peek()
26625 )));
26626 }
26627 self.advance();
26628 return Ok(Expr::FunctionCall {
26629 name: String::from("strpos"),
26630 args: alloc::vec![haystack, needle],
26631 });
26632 }
26633 // position(sub, str) comma form (incl. bytea) —
26634 // hand the parsed first arg to the generic list.
26635 args.push(needle);
26636 if matches!(self.peek(), Token::Comma) {
26637 self.advance();
26638 }
26639 }
26640 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26641 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26642 // plain comma forms TRIM(str) / TRIM(str, chars) keep
26643 // riding the generic argument list below.
26644 if first.eq_ignore_ascii_case("trim") {
26645 let mode = match self.peek() {
26646 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26647 self.advance();
26648 Some("btrim")
26649 }
26650 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26651 self.advance();
26652 Some("ltrim")
26653 }
26654 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26655 self.advance();
26656 Some("rtrim")
26657 }
26658 _ => None,
26659 };
26660 if mode.is_some() || matches!(self.peek(), Token::From) {
26661 // TRIM([mode] FROM str) — no strip-chars.
26662 let chars = if matches!(self.peek(), Token::From) {
26663 None
26664 } else {
26665 Some(self.parse_expr(0)?)
26666 };
26667 if !matches!(self.peek(), Token::From) {
26668 return Err(self.err(format!(
26669 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26670 self.peek()
26671 )));
26672 }
26673 self.advance();
26674 let target = self.parse_expr(0)?;
26675 if !matches!(self.peek(), Token::RParen) {
26676 return Err(
26677 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26678 );
26679 }
26680 self.advance();
26681 let mut trim_args = alloc::vec![target];
26682 if let Some(c) = chars {
26683 trim_args.push(c);
26684 }
26685 return Ok(Expr::FunctionCall {
26686 name: String::from(mode.unwrap_or("btrim")),
26687 args: trim_args,
26688 });
26689 }
26690 }
26691 if !matches!(self.peek(), Token::RParen) {
26692 loop {
26693 // v7.38 (read01, T14) — `argname => value` names this arg.
26694 // v7.39 (read01 round 77) — `argname := value` is the same
26695 // thing, and it is the spelling PG's own docs lead with. It
26696 // was simply never lexed here, so every `f(x := 1)` died in
26697 // the parser regardless of what `f` was.
26698 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26699 (
26700 Token::Ident(n) | Token::QuotedIdent(n),
26701 Some(Token::FatArrow | Token::ColonEq),
26702 ) => {
26703 let name = n.clone();
26704 self.advance(); // name
26705 self.advance(); // => / :=
26706 Some(name)
26707 }
26708 _ => None,
26709 };
26710 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26711 // array's elements into a variadic call's trailing args
26712 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26713 // reserved, so it arrives as a bare ident before the arg.
26714 let is_variadic = this_name.is_none()
26715 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26716 if is_variadic {
26717 self.advance();
26718 }
26719 let arg = self.parse_expr(0)?;
26720 args.push(match &this_name {
26721 // The callee's parameter names decide the slot, and a
26722 // user function's live in the catalog. Carry the name
26723 // to eval rather than guessing here.
26724 Some(n) => Expr::NamedArg {
26725 name: n.clone(),
26726 expr: Box::new(arg),
26727 },
26728 None if is_variadic => Expr::Variadic(Box::new(arg)),
26729 None => arg,
26730 });
26731 arg_names.push(this_name);
26732 // v7.25 (round-17) — standard `CAST(expr AS type)`.
26733 // The `::` cast already worked; this lowers the
26734 // function form onto the same Expr::Cast node.
26735 if first.eq_ignore_ascii_case("cast")
26736 && args.len() == 1
26737 && matches!(self.peek(), Token::As)
26738 {
26739 self.advance();
26740 let target = self.parse_cast_target()?;
26741 if !matches!(self.peek(), Token::RParen) {
26742 return Err(self.err(format!(
26743 "expected ')' to close CAST, got {:?}",
26744 self.peek()
26745 )));
26746 }
26747 self.advance();
26748 return Ok(Expr::Cast {
26749 expr: Box::new(args.pop().expect("one arg")),
26750 target,
26751 });
26752 }
26753 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26754 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26755 // keywords; SPG's lexer makes them plain idents (so they'd be
26756 // read as column refs). Lower the keyword to the string form
26757 // the evaluator already accepts.
26758 if first.eq_ignore_ascii_case("normalize")
26759 && args.len() == 1
26760 && matches!(self.peek(), Token::Comma)
26761 {
26762 let form = match self.tokens.get(self.pos + 1) {
26763 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26764 let up = f.to_ascii_uppercase();
26765 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26766 }
26767 _ => None,
26768 };
26769 if let Some(up) = form {
26770 self.advance(); // comma
26771 self.advance(); // form keyword
26772 args.push(Expr::Literal(Literal::String(up)));
26773 }
26774 }
26775 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26776 // form. Desugars to the comma-list shape evaluator already
26777 // handles. Triggered after the first arg when the function
26778 // name is substring / substr and the next token is FROM
26779 // (a reserved keyword in PG; SPG also reserves it).
26780 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26781 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26782 // internal __substring_similar(str, pat, esc) call.
26783 if (first.eq_ignore_ascii_case("substring")
26784 || first.eq_ignore_ascii_case("substr"))
26785 && args.len() == 1
26786 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26787 {
26788 self.advance(); // SIMILAR
26789 let pattern = self.parse_expr(0)?;
26790 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26791 {
26792 return Err(self.err(format!(
26793 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26794 self.peek()
26795 )));
26796 }
26797 self.advance(); // ESCAPE
26798 let esc = self.parse_expr(0)?;
26799 if !matches!(self.peek(), Token::RParen) {
26800 return Err(self.err(format!(
26801 "expected ')' to close substring(... SIMILAR ...), got {:?}",
26802 self.peek()
26803 )));
26804 }
26805 self.advance();
26806 args.push(pattern);
26807 args.push(esc);
26808 return Ok(Expr::FunctionCall {
26809 name: "__substring_similar".to_string(),
26810 args,
26811 });
26812 }
26813 if (first.eq_ignore_ascii_case("substring")
26814 || first.eq_ignore_ascii_case("substr"))
26815 && args.len() == 1
26816 && matches!(self.peek(), Token::From | Token::For)
26817 {
26818 // `substring(str FROM pos [FOR len])`, or the FOR-only
26819 // `substring(str FOR len)` which PG treats as FROM 1.
26820 if matches!(self.peek(), Token::From) {
26821 self.advance();
26822 let start = self.parse_expr(0)?;
26823 args.push(start);
26824 } else {
26825 args.push(Expr::Literal(Literal::Integer(1)));
26826 }
26827 if matches!(self.peek(), Token::For) {
26828 self.advance();
26829 let length = self.parse_expr(0)?;
26830 args.push(length);
26831 }
26832 if !matches!(self.peek(), Token::RParen) {
26833 return Err(self.err(format!(
26834 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26835 self.peek()
26836 )));
26837 }
26838 self.advance();
26839 return Ok(Expr::FunctionCall {
26840 name: first.to_ascii_lowercase(),
26841 args,
26842 });
26843 }
26844 // PG `overlay(str PLACING repl FROM n [FOR len])`
26845 // syntactic form. Desugars to the `overlay(str,
26846 // repl, n[, len])` comma-list shape the evaluator
26847 // already implements. `PLACING` is not a reserved
26848 // token in SPG, so it arrives as a bare Ident.
26849 if first.eq_ignore_ascii_case("overlay")
26850 && args.len() == 1
26851 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26852 {
26853 self.advance(); // consume PLACING
26854 args.push(self.parse_expr(0)?); // replacement
26855 if !matches!(self.peek(), Token::From) {
26856 return Err(self.err(format!(
26857 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26858 self.peek()
26859 )));
26860 }
26861 self.advance();
26862 args.push(self.parse_expr(0)?); // start position
26863 if matches!(self.peek(), Token::For) {
26864 self.advance();
26865 args.push(self.parse_expr(0)?); // length
26866 }
26867 if !matches!(self.peek(), Token::RParen) {
26868 return Err(self.err(format!(
26869 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26870 self.peek()
26871 )));
26872 }
26873 self.advance();
26874 return Ok(Expr::FunctionCall {
26875 name: String::from("overlay"),
26876 args,
26877 });
26878 }
26879 // `TRIM(chars FROM str)` — the keyword-less
26880 // spelling lands here after the chars parse
26881 // (the keyword forms return earlier).
26882 if first.eq_ignore_ascii_case("trim")
26883 && args.len() == 1
26884 && matches!(self.peek(), Token::From)
26885 {
26886 self.advance();
26887 let target = self.parse_expr(0)?;
26888 if !matches!(self.peek(), Token::RParen) {
26889 return Err(self.err(format!(
26890 "expected ')' to close TRIM(chars FROM str), got {:?}",
26891 self.peek()
26892 )));
26893 }
26894 self.advance();
26895 let chars = args.pop().expect("one arg");
26896 return Ok(Expr::FunctionCall {
26897 name: String::from("btrim"),
26898 args: alloc::vec![target, chars],
26899 });
26900 }
26901 // v7.24 (round-16 A) — aggregate-internal
26902 // ordering: `array_agg(x ORDER BY y DESC NULLS
26903 // LAST)`. Keys close the argument list.
26904 if matches!(self.peek(), Token::Order) {
26905 self.advance();
26906 if !self.peek_is_by() {
26907 return Err(self.err(format!(
26908 "expected BY after ORDER in aggregate args, got {:?}",
26909 self.peek()
26910 )));
26911 }
26912 self.advance();
26913 loop {
26914 // v7.39 (round 691) — save/restore, the discipline this parser
26915 // already uses around `pending_sample_preds`, so a subquery inside
26916 // a key neither inherits nor leaks the channel.
26917 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26918 let saved_coll = self.order_key_collation.take();
26919 let parsed = self.parse_expr(0);
26920 self.in_order_by_key = saved_flag;
26921 let collation =
26922 core::mem::replace(&mut self.order_key_collation, saved_coll);
26923 let expr = parsed?;
26924 let desc = if matches!(self.peek(), Token::Desc) {
26925 self.advance();
26926 true
26927 } else if matches!(self.peek(), Token::Asc) {
26928 self.advance();
26929 false
26930 } else {
26931 false
26932 };
26933 let nulls_first = self.parse_optional_nulls_placement()?;
26934 agg_order_by.push(OrderBy {
26935 expr,
26936 desc,
26937 nulls_first,
26938 collation,
26939 });
26940 if matches!(self.peek(), Token::Comma) {
26941 self.advance();
26942 } else {
26943 break;
26944 }
26945 }
26946 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26947 // follow the ORDER BY inside GROUP_CONCAT.
26948 if self.consume_group_concat_separator(&mut args)? {
26949 saw_separator = true;
26950 }
26951 if !matches!(self.peek(), Token::RParen) {
26952 return Err(self.err(format!(
26953 "expected ')' after aggregate ORDER BY, got {:?}",
26954 self.peek()
26955 )));
26956 }
26957 break;
26958 }
26959 // v7.39 (round 354, M12) — …or directly after the
26960 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26961 // own spelling of what PG passes as string_agg's second
26962 // argument; it was a parse error, so every MySQL query
26963 // that names its own separator failed outright.
26964 if self.consume_group_concat_separator(&mut args)? {
26965 saw_separator = true;
26966 break;
26967 }
26968 match self.peek() {
26969 Token::Comma => {
26970 self.advance();
26971 }
26972 Token::RParen => break,
26973 other => {
26974 return Err(self.err(format!(
26975 "expected ',' or ')' in function args, got {other:?}"
26976 )));
26977 }
26978 }
26979 }
26980 }
26981 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26982 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26983 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26984 // meaning a separator — that is what the explicit SEPARATOR
26985 // tail is for. Fold them into one `concat(...)` so the
26986 // aggregate keeps its single value argument.
26987 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26988 let values = args.len() - usize::from(saw_separator);
26989 if values > 1 {
26990 let sep_arg = if saw_separator { args.pop() } else { None };
26991 let folded = Expr::FunctionCall {
26992 name: "concat".to_string(),
26993 args: core::mem::take(&mut args),
26994 };
26995 args.push(folded);
26996 if let Some(sep) = sep_arg {
26997 args.push(sep);
26998 }
26999 }
27000 }
27001 self.advance(); // consume ')'
27002 // v7.39 (read01 round 77) — named arguments are NOT reordered here
27003 // any more. The parser has no catalog, so it could only ever resolve
27004 // the handful of `make_*` builtins whose parameter names were baked
27005 // into a table right here — every user function got
27006 // "does not support named arguments", though the catalog has been
27007 // storing its parameter names all along. Reordering happens in eval,
27008 // in one place, for builtins and user functions alike.
27009 // v7.32 (round-29) — ordered-set aggregate tail
27010 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
27011 // (percentile_cont / percentile_disc / mode). The sort spec
27012 // lands in the same `order_by` slot a decorated aggregate
27013 // uses; the executor dispatches on the function name. WITHIN
27014 // GROUP and an intra-argument ORDER BY are mutually
27015 // exclusive (PG rejects both).
27016 let within_group_order = self.parse_within_group_clause()?;
27017 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
27018 return Err(self.err(
27019 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
27020 .into(),
27021 ));
27022 }
27023 let within_group_seen = !within_group_order.is_empty();
27024 let agg_order_by = if within_group_order.is_empty() {
27025 agg_order_by
27026 } else {
27027 within_group_order
27028 };
27029 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
27030 let filter = self.parse_filter_clause()?;
27031 // v4.12: window-function tail — `name(args) OVER (...)`.
27032 // Promotes the just-parsed FunctionCall into a
27033 // WindowFunction node carrying partition + order.
27034 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
27035 // / `RESPECT NULLS OVER (...)` between the closing paren
27036 // and `OVER`.
27037 let null_treatment = self.parse_null_treatment_modifier();
27038 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
27039 && s.eq_ignore_ascii_case("over")
27040 {
27041 self.advance();
27042 // v7.39 (round 230) — PG implements neither modifier for a
27043 // windowed call and says so (0A000). Both used to be parsed
27044 // and then silently dropped here, so `count(DISTINCT v)
27045 // OVER (…)` quietly answered the non-distinct count.
27046 if agg_distinct {
27047 return Err(
27048 self.err("DISTINCT is not implemented for window functions".to_string())
27049 );
27050 }
27051 if !agg_order_by.is_empty() {
27052 // PG separates the two shapes that land here: a
27053 // WITHIN GROUP call is an ordered-set aggregate and gets
27054 // its own message naming the aggregate; a plain
27055 // `agg(x ORDER BY y)` gets the generic one.
27056 let msg = if within_group_seen {
27057 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
27058 } else {
27059 "aggregate ORDER BY is not implemented for window functions".to_string()
27060 };
27061 return Err(self.err(msg));
27062 }
27063 let (partition_by, order_by, frame) = self.parse_over_clause()?;
27064 return Ok(Expr::WindowFunction {
27065 name: first,
27066 args,
27067 partition_by,
27068 order_by,
27069 frame,
27070 null_treatment,
27071 filter,
27072 });
27073 }
27074 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
27075 return Ok(Expr::AggregateOrdered {
27076 call: Box::new(Expr::FunctionCall { name: first, args }),
27077 order_by: agg_order_by,
27078 distinct: agg_distinct,
27079 filter,
27080 });
27081 }
27082 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
27083 // over TIMESTAMPTZ and has no timestamp overload, so a
27084 // timestamp argument is coerced on the way in and the answer
27085 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
27086 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
27087 // zone`. SPG answered `timestamp without time zone`, dropping
27088 // the offset from every rendering.
27089 //
27090 // Writing the coercion PG performs makes the existing
27091 // argument-driven typing (the one `date_trunc` uses) reach the
27092 // right answer, rather than teaching the type layer a second
27093 // rule. MySQL's DATE_ADD is a different function that returns
27094 // DATE or DATETIME, so this is PG-dialect only.
27095 //
27096 // Out-of-line because this sits on the RECURSIVE descent
27097 // frame: an inline block with locals here costs every nesting
27098 // level, and the suite's deep-nesting sentinel overflowed the
27099 // 512 KiB parser stack the moment one was added (round 430's
27100 // lesson, in the same shape).
27101 if !self.mysql_dialect {
27102 lift_date_add_arg_to_timestamptz(&first, &mut args);
27103 }
27104 return Ok(Expr::FunctionCall { name: first, args });
27105 }
27106 // v7.9.20 — SQL-standard parenless keyword expressions
27107 // (PG treats these as functions called without parens).
27108 // Resolve to a synthetic FunctionCall so the engine's
27109 // eval path reuses the existing function-call routing.
27110 // mailrs G3.
27111 let lc = first.to_ascii_lowercase();
27112 if matches!(
27113 lc.as_str(),
27114 "current_date"
27115 | "current_time"
27116 | "current_timestamp"
27117 | "localtimestamp"
27118 | "localtime"
27119 // v7.37.17 (17.6 siblings) — session-identity SQL-
27120 // standard parenless keywords. current_user /
27121 // session_user / user were already caught by the
27122 // pgwire canned-response shortcut but bare-select
27123 // in the embedded engine went through Expr::Column
27124 // and errored. Adding them here so the parser
27125 // resolves to a synthetic FunctionCall that reuses
27126 // the existing eval/functions.rs dispatch.
27127 | "current_user"
27128 | "session_user"
27129 | "current_role"
27130 | "current_catalog"
27131 | "current_schema"
27132 | "current_database"
27133 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27134 | "system_user"
27135 ) {
27136 return Ok(Expr::FunctionCall {
27137 name: lc,
27138 args: Vec::new(),
27139 });
27140 }
27141 Ok(Expr::Column(ColumnName {
27142 qualifier: None,
27143 name: first,
27144 }))
27145 }
27146}
27147
27148/// v7.39 (round 522) — write the coercion PG's `date_add` /
27149/// `date_subtract` signature performs.
27150///
27151/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27152/// timestamp argument is cast on the way in and the answer is
27153/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27154/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27155/// `timestamp without time zone`, dropping the offset from every
27156/// rendering of the result.
27157///
27158/// Writing the cast the signature implies lets the existing
27159/// argument-driven typing (the one `date_trunc` uses) reach the right
27160/// answer instead of teaching the type layer a second rule. MySQL's
27161/// DATE_ADD is a different function returning DATE or DATETIME, so the
27162/// caller applies this in PG dialect only.
27163///
27164/// A free function, and not a block at the call site, because the caller
27165/// is on the recursive-descent frame chain.
27166#[inline(never)]
27167fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27168 if args.len() != 2
27169 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27170 {
27171 return;
27172 }
27173 let base = args.remove(0);
27174 args.insert(
27175 0,
27176 Expr::Cast {
27177 expr: Box::new(base),
27178 target: CastTarget::Timestamptz,
27179 },
27180 );
27181}
27182
27183/// v6.8.2 — walk an expression tree and return the first column
27184/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27185/// to derive `CreateIndexStatement.column` from an expression
27186/// key (so downstream planner code resolving a primary column
27187/// position keeps working with expression indexes). Returns
27188/// `None` when the expression has no column ref at all — caller
27189/// surfaces that as a parse error.
27190fn extract_first_column(expr: &Expr) -> Option<String> {
27191 match expr {
27192 Expr::Column(cn) => Some(cn.name.clone()),
27193 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27194 Expr::Binary { lhs, rhs, .. } => {
27195 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27196 }
27197 Expr::Unary { expr: e, .. } => extract_first_column(e),
27198 // v7.39 (read01 round 93) — a cast wraps its operand: a common
27199 // expression-index key is `lower(col::text)`, where the column
27200 // sits under the `::text` cast inside the function arg. Without
27201 // descending here the key was rejected as "references no column".
27202 Expr::Cast { expr: e, .. } => extract_first_column(e),
27203 // v7.39.2 — and a COLLATE wraps its operand the same way.
27204 // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27205 // column the moment the clause became a node instead of being
27206 // absorbed, and the key was rejected as referencing none. This
27207 // is the shape the wildcard below silently produces, which is
27208 // why it is spelled out.
27209 Expr::Collate { expr: e, .. } => extract_first_column(e),
27210 _ => None,
27211 }
27212}
27213
27214fn maybe_not(expr: Expr, negated: bool) -> Expr {
27215 if negated {
27216 Expr::Unary {
27217 op: UnOp::Not,
27218 expr: Box::new(expr),
27219 }
27220 } else {
27221 expr
27222 }
27223}
27224
27225/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27226/// things in the two dialects, and SPG read all three PG's way:
27227///
27228/// | token | PG (and SPG) | MySQL, measured |
27229/// |---|---|---|
27230/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27231/// | `&&` | inet / array overlap | **AND** |
27232/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27233///
27234/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27235/// answer with no error, which is why they are routed here rather than
27236/// left to the shared table.
27237impl Parser {
27238 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27239 if self.mysql_dialect {
27240 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27241 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27242 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27243 if let Token::Ident(w) = tok
27244 && w.eq_ignore_ascii_case("div")
27245 {
27246 return Some((BinOp::IntDiv, 8));
27247 }
27248 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27249 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27250 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27251 // there sits in operand position, not infix).
27252 if let Token::Ident(w) = tok
27253 && w.eq_ignore_ascii_case("mod")
27254 {
27255 return Some((BinOp::Mod, 8));
27256 }
27257 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27258 // plain ident to the lexer. Its precedence sits between OR (1)
27259 // and AND (3) — hence rung 2, the slot freed by moving AND up.
27260 if let Token::Ident(w) = tok
27261 && w.eq_ignore_ascii_case("xor")
27262 {
27263 return Some((BinOp::LogicalXor, 2));
27264 }
27265 match tok {
27266 Token::Concat => return Some((BinOp::Or, 1)),
27267 // MySQL's `&&` is logical AND, sharing AND's rung (3).
27268 Token::InetOverlap => return Some((BinOp::And, 3)),
27269 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27270 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27271 _ => {}
27272 }
27273 }
27274 binop_from(tok)
27275 }
27276}
27277
27278// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27279// (which sits strictly between OR and AND), every level from AND upward was
27280// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27281// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27282// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27283// the *relative* order of every PG operator is unchanged by the shift.
27284fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27285 let pair = match tok {
27286 Token::Or => (BinOp::Or, 1),
27287 Token::And => (BinOp::And, 3),
27288 Token::Eq => (BinOp::Eq, 5),
27289 Token::NotEq => (BinOp::NotEq, 5),
27290 Token::Lt => (BinOp::Lt, 5),
27291 Token::LtEq => (BinOp::LtEq, 5),
27292 Token::Gt => (BinOp::Gt, 5),
27293 Token::GtEq => (BinOp::GtEq, 5),
27294 // pgvector distance ops all sit on the same rung — tighter than
27295 // comparisons (5) so `col <-> v < threshold` parses correctly.
27296 Token::L2Distance => (BinOp::L2Distance, 6),
27297 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27298 // comparison rung.
27299 Token::GeomParallel => (BinOp::GeomParallel, 5),
27300 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27301 // comparison rung.
27302 Token::OverLeft => (BinOp::OverLeft, 5),
27303 Token::OverRight => (BinOp::OverRight, 5),
27304 Token::GeomPerp => (BinOp::GeomPerp, 5),
27305 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27306 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27307 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27308 Token::InnerProduct => (BinOp::InnerProduct, 6),
27309 Token::CosineDistance => (BinOp::CosineDistance, 6),
27310 Token::Plus => (BinOp::Add, 7),
27311 Token::Minus => (BinOp::Sub, 7),
27312 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27313 // binds every "other" operator (`||`, `|`, `&`, `#`, the
27314 // pgvector distances above) BETWEEN additive (7) and the
27315 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27316 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27317 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27318 // ("matches PG conceptually" — the round-753 audit measured it
27319 // false; the old rung errored on `'a' || 1 + 1` with
27320 // `text + integer`). Same-level chains left-fold, as PG does.
27321 Token::Concat => (BinOp::Concat, 6),
27322 Token::Pipe => (BinOp::BitOr, 6),
27323 Token::Amp => (BinOp::BitAnd, 6),
27324 Token::Star => (BinOp::Mul, 8),
27325 Token::Slash => (BinOp::Div, 8),
27326 Token::Percent => (BinOp::Mod, 8),
27327 // v4.14: JSON path ops bind tighter than comparisons (5)
27328 // and additive (7) so `doc->'k' = 'v'` parses correctly.
27329 // Same rung as the multiplicative ops.
27330 Token::JsonGet => (BinOp::JsonGet, 8),
27331 Token::JsonGetText => (BinOp::JsonGetText, 8),
27332 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27333 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27334 Token::JsonContains => (BinOp::JsonContains, 8),
27335 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27336 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27337 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27338 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27339 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27340 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27341 // v7.12.2 — `@@` binds at the comparison rung (looser than
27342 // arithmetic, tighter than AND / OR). PG places `@@` at
27343 // the same precedence as `=` / `<`, so we follow.
27344 Token::TsMatch => (BinOp::TsMatch, 5),
27345 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27346 // PG places these at the comparison rung (same level as `=`),
27347 // so we follow.
27348 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27349 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27350 Token::InetContains => (BinOp::InetContains, 5),
27351 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27352 Token::InetOverlap => (BinOp::InetOverlap, 5),
27353 // v7.39 (round 508) — the geometric and pattern-order predicates
27354 // ride the comparison rung, as every other predicate does.
27355 Token::Intersects => (BinOp::Intersects, 5),
27356 Token::IsBelow => (BinOp::IsBelow, 5),
27357 Token::IsAbove => (BinOp::IsAbove, 5),
27358 Token::PatternLt => (BinOp::PatternLt, 5),
27359 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27360 Token::PatternGt => (BinOp::PatternGt, 5),
27361 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27362 // `@@@` is the old spelling of `@@` and means exactly it.
27363 Token::TsMatchOld => (BinOp::TsMatch, 5),
27364 _ => return None,
27365 };
27366 Some(pair)
27367}
27368
27369#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27370// `as f32` here is intentional: vector elements widen / narrow into f32 on
27371// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27372// past ~15 decimal digits — both are acceptable for a fixed-precision
27373// pgvector column.
27374/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27375/// implicit table alias and break trailing clauses. WITH lands
27376/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27377/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27378/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27379/// / VALUES / FOR / LATERAL — all of which would otherwise be
27380/// silently swallowed by `parse_optional_alias`.
27381fn is_alias_stopword(s: &str) -> bool {
27382 matches!(
27383 s.to_ascii_lowercase().as_str(),
27384 "with"
27385 | "on"
27386 | "where"
27387 | "having"
27388 | "group"
27389 | "order"
27390 | "limit"
27391 | "offset"
27392 | "union"
27393 | "except"
27394 | "intersect"
27395 | "returning"
27396 | "set"
27397 | "values"
27398 | "for"
27399 | "window"
27400 | "tablesample"
27401 | "lateral"
27402 | "left"
27403 | "right"
27404 | "inner"
27405 | "outer"
27406 | "full"
27407 | "cross"
27408 | "join"
27409 | "natural"
27410 | "using"
27411 | "fetch"
27412 )
27413}
27414
27415fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27416 match e {
27417 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27418 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27419 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27420 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27421 // so scale the divisor by hand instead of `f32::powi`.)
27422 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27423 let mut div = 1.0f32;
27424 for _ in 0..*scale {
27425 div *= 10.0;
27426 }
27427 Some(*unscaled as f32 / div)
27428 }
27429 Expr::Unary {
27430 op: UnOp::Neg,
27431 expr,
27432 } => extract_numeric_literal(expr).map(|x| -x),
27433 _ => None,
27434 }
27435}
27436
27437/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27438/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27439/// negative. Returns `None` if any pair fails to parse or no pair is found.
27440///
27441/// Recognised units (case-insensitive, optional trailing `s`):
27442/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27443/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27444/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27445/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27446/// (PG-canonical: DST and month-boundary semantics depend on this).
27447/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27448/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27449/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27450#[allow(clippy::cast_possible_truncation)]
27451fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27452 let mut months: i64 = 0;
27453 let mut days: i64 = 0;
27454 let mut micros: i64 = 0;
27455 let mut in_time = false;
27456 let mut num = alloc::string::String::new();
27457 for ch in rest.chars() {
27458 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27459 num.push(ch);
27460 continue;
27461 }
27462 if ch == 'T' || ch == 't' {
27463 if !num.is_empty() {
27464 return None;
27465 }
27466 in_time = true;
27467 continue;
27468 }
27469 let n: f64 = num.parse().ok()?;
27470 num.clear();
27471 match (ch, in_time) {
27472 ('Y' | 'y', false) => months += (n * 12.0) as i64,
27473 ('M', false) => months += n as i64,
27474 ('W' | 'w', false) => days += (n * 7.0) as i64,
27475 ('D' | 'd', false) => days += n as i64,
27476 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27477 ('M', true) => micros += (n * 60_000_000.0) as i64,
27478 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27479 _ => return None,
27480 }
27481 }
27482 if !num.is_empty() {
27483 return None;
27484 }
27485 Some((
27486 i32::try_from(months).ok()?,
27487 i32::try_from(days).ok()?,
27488 micros,
27489 ))
27490}
27491
27492/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27493/// leading `-` negates the whole value). Rejects date-like strings.
27494fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27495 let (neg, body) = match s.strip_prefix('-') {
27496 Some(b) => (true, b),
27497 None => (false, s),
27498 };
27499 let (y, m) = body.split_once('-')?;
27500 let years: i32 = y.parse().ok()?;
27501 let mons: i32 = m.parse().ok()?;
27502 if years < 0 || mons < 0 {
27503 return None;
27504 }
27505 let total = years.checked_mul(12)?.checked_add(mons)?;
27506 Some((if neg { -total } else { total }, 0, 0))
27507}
27508
27509/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27510/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27511fn parse_interval_clock(tok: &str) -> Option<i64> {
27512 let (neg, body) = match tok.strip_prefix('-') {
27513 Some(r) => (true, r),
27514 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27515 };
27516 let mut it = body.split(':');
27517 let h: i64 = it.next()?.parse().ok()?;
27518 let m: i64 = it.next()?.parse().ok()?;
27519 let s_tok = it.next().unwrap_or("0");
27520 if it.next().is_some() {
27521 return None;
27522 }
27523 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27524 let sec: i64 = sec.parse().ok()?;
27525 let mut f = alloc::string::String::from(frac);
27526 while f.len() < 6 {
27527 f.push('0');
27528 }
27529 f.truncate(6);
27530 let fus: i64 = f.parse().ok()?;
27531 sec.checked_mul(1_000_000)?.checked_add(fus)?
27532 } else {
27533 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27534 };
27535 let total = h
27536 .checked_mul(3_600_000_000)?
27537 .checked_add(m.checked_mul(60_000_000)?)?
27538 .checked_add(sec_us)?;
27539 Some(if neg { -total } else { total })
27540}
27541
27542/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27543/// every spelling PG accepts (measured against live PG18.4, not guessed):
27544/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27545/// Before this, the unit table matched long names only, with an ad-hoc
27546/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27547/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27548/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27549/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27550/// fractional) both read from this one table now.
27551fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27552 let u = raw.to_ascii_lowercase();
27553 Some(match u.as_str() {
27554 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27555 "microsecond"
27556 }
27557 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27558 "millisecond"
27559 }
27560 "second" | "seconds" | "sec" | "secs" | "s" => "second",
27561 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27562 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27563 "day" | "days" | "d" => "day",
27564 "week" | "weeks" | "w" => "week",
27565 "month" | "months" | "mon" | "mons" => "month",
27566 "year" | "years" | "yr" | "yrs" | "y" => "year",
27567 "decade" | "decades" | "dec" | "decs" => "decade",
27568 "century" | "centuries" | "cent" | "c" => "century",
27569 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27570 _ => return None,
27571 })
27572}
27573
27574/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27575/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27577pub(crate) enum IntervalField {
27578 Year,
27579 Month,
27580 Day,
27581 Hour,
27582 Minute,
27583 Second,
27584}
27585
27586/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27587/// spellings aren't standard for the qualifier position, so only the singular
27588/// forms are accepted.
27589/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27590/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27591/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27592/// take a `'1 2'` style literal — are not read here; they stay a parse
27593/// error rather than being silently misread.)
27594/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27595///
27596/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27597/// to do with a `@@` engine setting, and an unset one reads NULL rather
27598/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27599/// were the same node and `SELECT @x` answered "Unknown system variable".)
27600/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27601/// not see a session override — measured, after `SET autocommit=0`,
27602/// `@@global.autocommit` is still 1.
27603///
27604/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27605/// the parser's nesting budget is tuned against, and building these
27606/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27607/// wall `parse_left_right_atom` and friends were factored out for).
27608#[inline(never)]
27609fn variable_ref_atom(raw: &str) -> Expr {
27610 let user_var = !raw.starts_with("@@");
27611 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27612 Expr::FunctionCall {
27613 name: String::from(if user_var {
27614 "__spg_user_var"
27615 } else {
27616 "__spg_session_var"
27617 }),
27618 args: alloc::vec![Expr::Literal(Literal::String(bare))],
27619 }
27620}
27621
27622fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27623 let Token::Ident(s) = tok else { return None };
27624 Some(match () {
27625 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27626 () if s.eq_ignore_ascii_case("second") => "second",
27627 () if s.eq_ignore_ascii_case("minute") => "minute",
27628 () if s.eq_ignore_ascii_case("hour") => "hour",
27629 () if s.eq_ignore_ascii_case("day") => "day",
27630 () if s.eq_ignore_ascii_case("week") => "week",
27631 () if s.eq_ignore_ascii_case("month") => "month",
27632 () if s.eq_ignore_ascii_case("quarter") => "quarter",
27633 () if s.eq_ignore_ascii_case("year") => "year",
27634 () => return None,
27635 })
27636}
27637
27638/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27639/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27640/// which constructs the value at run time. Only the slot the unit names
27641/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27642/// slot the builtin has (months and fractional seconds respectively).
27643fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27644 let zero = || Expr::Literal(Literal::Integer(0));
27645 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27646 lhs: alloc::boxed::Box::new(qty.clone()),
27647 op,
27648 rhs: alloc::boxed::Box::new(by),
27649 };
27650 // (years, months, weeks, days, hours, mins, secs)
27651 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27652 match unit {
27653 "year" => args[0] = qty,
27654 "quarter" => {
27655 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27656 }
27657 "month" => args[1] = qty,
27658 "week" => args[2] = qty,
27659 "day" => args[3] = qty,
27660 "hour" => args[4] = qty,
27661 "minute" => args[5] = qty,
27662 "second" => args[6] = qty,
27663 // The builtin's seconds slot takes a fraction, so microseconds ride
27664 // it scaled down; the divisor is a NUMERIC literal so the division
27665 // stays exact rather than going through a float.
27666 "microsecond" => {
27667 args[6] = scaled(
27668 crate::ast::BinOp::Div,
27669 Expr::Literal(Literal::Numeric {
27670 unscaled: 1_000_000,
27671 scale: 0,
27672 }),
27673 );
27674 }
27675 _ => args[3] = qty,
27676 }
27677 Expr::FunctionCall {
27678 name: alloc::string::String::from("make_interval"),
27679 args,
27680 }
27681}
27682
27683/// `(count, unit)` → `(months, days, micros)`.
27684fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27685 let n: i64 = count.trim().parse().ok()?;
27686 Some(match unit {
27687 "microsecond" => (0, 0, n),
27688 "second" => (0, 0, n.checked_mul(1_000_000)?),
27689 "minute" => (0, 0, n.checked_mul(60_000_000)?),
27690 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27691 "day" => (0, i32::try_from(n).ok()?, 0),
27692 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27693 "month" => (i32::try_from(n).ok()?, 0, 0),
27694 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27695 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27696 _ => return None,
27697 })
27698}
27699
27700fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27701 let Token::Ident(s) = tok else { return None };
27702 Some(match () {
27703 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27704 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27705 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27706 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27707 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27708 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27709 () => return None,
27710 })
27711}
27712
27713/// v7.39 (read01 round 102) — interpret an interval literal under a field
27714/// qualifier. Returns `(months, days, micros)`.
27715///
27716/// * A single field applied to a bare number sets which unit the number means,
27717/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27718/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27719/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27720/// * Every other range, and any literal a single field can't read as a plain
27721/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27722/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27723/// like PG, and the qualifier there only bounds precision.
27724fn interpret_qualified_interval(
27725 text: &str,
27726 (f1, f2): (IntervalField, Option<IntervalField>),
27727) -> Option<(i32, i32, i64)> {
27728 if let Some(f2) = f2 {
27729 if f1 == IntervalField::Year && f2 == IntervalField::Month {
27730 if let Some(m) = parse_year_month_literal(text) {
27731 return Some((m, 0, 0));
27732 }
27733 }
27734 return parse_interval_text(text);
27735 }
27736 // Single field: reinterpret a bare number; otherwise the default parse.
27737 let trimmed = text.trim();
27738 if let Ok(val) = trimmed.parse::<f64>() {
27739 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27740 #[allow(clippy::cast_possible_truncation)]
27741 let whole = val as i64;
27742 #[allow(clippy::cast_possible_truncation)]
27743 let secs_micros = {
27744 let m = val * 1_000_000.0;
27745 if m >= 0.0 {
27746 (m + 0.5) as i64
27747 } else {
27748 (m - 0.5) as i64
27749 }
27750 };
27751 return Some(match f1 {
27752 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27753 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27754 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27755 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27756 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27757 IntervalField::Second => (0, 0, secs_micros),
27758 });
27759 }
27760 parse_interval_text(text)
27761}
27762
27763/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27764fn parse_year_month_literal(text: &str) -> Option<i32> {
27765 let t = text.trim();
27766 let (neg, body) = match t.strip_prefix('-') {
27767 Some(r) => (true, r),
27768 None => (false, t.strip_prefix('+').unwrap_or(t)),
27769 };
27770 let mut it = body.split('-');
27771 let years: i32 = it.next()?.trim().parse().ok()?;
27772 let months: i32 = match it.next() {
27773 Some(m) => m.trim().parse().ok()?,
27774 None => 0,
27775 };
27776 if it.next().is_some() {
27777 return None;
27778 }
27779 let total = years.checked_mul(12)?.checked_add(months)?;
27780 Some(if neg { -total } else { total })
27781}
27782
27783pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27784 // v7.38.19 — the two infinities, answered as the three extreme
27785 // fields PostgreSQL itself puts on the wire for them:
27786 //
27787 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27788 // … 7fffffffffffffff 7fffffff 7fffffff
27789 //
27790 // So no caller has to know the spelling — every one of them already
27791 // reads the three numbers, and `IntervalKind::from_fields` names
27792 // what they mean.
27793 //
27794 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27795 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27796 // infinity. Interval takes the full word, in any case.
27797 {
27798 let word = s.trim();
27799 let word = word.strip_prefix('@').map_or(word, str::trim);
27800 let (neg, body) = match word.strip_prefix('-') {
27801 Some(rest) => (true, rest.trim_start()),
27802 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27803 };
27804 if body.eq_ignore_ascii_case("infinity") {
27805 return Some(if neg {
27806 (i32::MIN, i32::MIN, i64::MIN)
27807 } else {
27808 (i32::MAX, i32::MAX, i64::MAX)
27809 });
27810 }
27811 }
27812 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27813 // `@` is decorative; a trailing `ago` negates the whole interval.
27814 let mut trimmed = s.trim();
27815 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27816 let mut negate = false;
27817 if let Some(rest) = trimmed
27818 .strip_suffix("ago")
27819 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27820 {
27821 negate = true;
27822 trimmed = rest.trim();
27823 }
27824 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27825 let (mo, d, us) = v?;
27826 if negate {
27827 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27828 } else {
27829 Some((mo, d, us))
27830 }
27831 };
27832 let s = trimmed;
27833 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27834 // are single tokens, not the `<n> <unit>` pair form handled below.
27835 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27836 return finish(parse_iso8601_interval(rest));
27837 }
27838 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27839 if let Some(iv) = parse_year_month_interval(trimmed) {
27840 return finish(Some(iv));
27841 }
27842 }
27843 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27844 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27845 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27846 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27847 if let Ok(n) = trimmed.parse::<i64>() {
27848 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27849 }
27850 if let Ok(f) = trimmed.parse::<f64>() {
27851 if f.is_finite() {
27852 #[allow(clippy::cast_possible_truncation)]
27853 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27854 }
27855 }
27856 }
27857 // v7.39 (round 243) — PG accepts the number and unit run together
27858 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27859 // the `<n> <unit>` pair loop below sees them as two.
27860 let raw_parts: Vec<&str> = s.split_whitespace().collect();
27861 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27862 for p in raw_parts {
27863 let boundary = p
27864 .char_indices()
27865 .find(|(i, c)| {
27866 *i > 0
27867 && c.is_ascii_alphabetic()
27868 && p[..*i]
27869 .chars()
27870 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27871 && p[..*i].chars().any(|d| d.is_ascii_digit())
27872 })
27873 .map(|(i, _)| i);
27874 match boundary {
27875 Some(i) => {
27876 parts.push(&p[..i]);
27877 parts.push(&p[i..]);
27878 }
27879 None => parts.push(p),
27880 }
27881 }
27882 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27883 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27884 // remains is the `<n> <unit>` pair form handled below.
27885 let mut clock_us: i64 = 0;
27886 let mut had_clock = false;
27887 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27888 clock_us = parse_interval_clock(parts[pos])?;
27889 parts.remove(pos);
27890 had_clock = true;
27891 }
27892 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27893 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27894 let mut lone_days: i32 = 0;
27895 if had_clock && parts.len() == 1 {
27896 if let Ok(n) = parts[0].parse::<i64>() {
27897 lone_days = i32::try_from(n).ok()?;
27898 parts.clear();
27899 }
27900 }
27901 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27902 return None;
27903 }
27904 let mut months: i32 = 0;
27905 let mut days: i32 = lone_days;
27906 let mut micros: i64 = clock_us;
27907 let mut i = 0;
27908 while i < parts.len() {
27909 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27910 if let Ok(n) = parts[i].parse::<i64>() {
27911 match unit_stripped {
27912 "microsecond" => micros = micros.checked_add(n)?,
27913 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27914 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27915 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27916 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27917 "day" => {
27918 let n32 = i32::try_from(n).ok()?;
27919 days = days.checked_add(n32)?;
27920 }
27921 "week" => {
27922 let n32 = i32::try_from(n).ok()?;
27923 days = days.checked_add(n32.checked_mul(7)?)?;
27924 }
27925 "month" => {
27926 let n32 = i32::try_from(n).ok()?;
27927 months = months.checked_add(n32)?;
27928 }
27929 "year" => {
27930 let n32 = i32::try_from(n).ok()?;
27931 months = months.checked_add(n32.checked_mul(12)?)?;
27932 }
27933 // v7.39 (read01 timestamp.c) — the larger calendar units.
27934 "decade" => {
27935 let n32 = i32::try_from(n).ok()?;
27936 months = months.checked_add(n32.checked_mul(120)?)?;
27937 }
27938 "century" => {
27939 let n32 = i32::try_from(n).ok()?;
27940 months = months.checked_add(n32.checked_mul(1200)?)?;
27941 }
27942 "millennium" => {
27943 let n32 = i32::try_from(n).ok()?;
27944 months = months.checked_add(n32.checked_mul(12000)?)?;
27945 }
27946 _ => return None,
27947 }
27948 } else if let Ok(f) = parts[i].parse::<f64>() {
27949 // Fractional units cascade down to the next-finer field the way
27950 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27951 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27952 // no_std: f64 has no trunc/fract/round methods, so do them with
27953 // casts (toward-zero) + explicit round-half-away-from-zero.
27954 #[allow(clippy::cast_possible_truncation)]
27955 fn round_i64(x: f64) -> i64 {
27956 if x >= 0.0 {
27957 (x + 0.5) as i64
27958 } else {
27959 (x - 0.5) as i64
27960 }
27961 }
27962 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27963 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27964 const DAY_US: f64 = 86_400_000_000.0;
27965 let whole = d as i64; // truncates toward zero
27966 let frac = d - whole as f64;
27967 *days = days.checked_add(i32::try_from(whole).ok()?)?;
27968 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27969 Some(())
27970 }
27971 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27972 match unit_stripped {
27973 "microsecond" => micros = micros.checked_add(round_i64(f))?,
27974 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27975 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27976 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27977 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27978 "day" => add_days_frac(&mut days, &mut micros, f)?,
27979 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27980 "month" => {
27981 let whole = f as i64;
27982 months = months.checked_add(i32::try_from(whole).ok()?)?;
27983 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27984 }
27985 "year" => {
27986 let m = f * 12.0;
27987 let whole = m as i64;
27988 months = months.checked_add(i32::try_from(whole).ok()?)?;
27989 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27990 }
27991 _ => return None,
27992 }
27993 } else {
27994 return None;
27995 }
27996 i += 2;
27997 }
27998 finish(Some((months, days, micros)))
27999}
28000
28001/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
28002/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
28003/// `interval` is intentionally absent (handled by its own parser arm).
28004/// Returns `None` for names that aren't sensible as a bare typed literal, so
28005/// the caller falls back to treating the ident as a column reference.
28006fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
28007 Some(match ident {
28008 "date" => CastTarget::Date,
28009 "timestamp" | "datetime" => CastTarget::Timestamp,
28010 "timestamptz" => CastTarget::Timestamptz,
28011 "bool" | "boolean" => CastTarget::Bool,
28012 "int" | "integer" | "int4" => CastTarget::Int,
28013 "bigint" | "int8" => CastTarget::BigInt,
28014 "float8" | "double precision" => CastTarget::Float,
28015 "uuid" => CastTarget::Uuid,
28016 "bytea" => CastTarget::Bytea,
28017 "json" => CastTarget::Json,
28018 "jsonb" => CastTarget::Jsonb,
28019 // Types without a dedicated CastTarget variant flow through the
28020 // generic Named path (engine resolves via column_type_to_data_type).
28021 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
28022 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
28023 | "money" | "bit" | "varbit"
28024 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
28025 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
28026 // Range / multirange types likewise.
28027 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
28028 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
28029 | "datemultirange" | "tsmultirange" | "tstzmultirange"
28030 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
28031 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
28032 CastTarget::Named(alloc::string::String::from(ident))
28033 }
28034 _ => return None,
28035 })
28036}
28037
28038/// v7.12.4 — map a bare type-name identifier (the form that
28039/// appears in a function arg list or RETURNS clause) to a
28040/// [`ColumnTypeName`]. Returns `None` for unknown / extension
28041/// types so the caller can preserve them as
28042/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
28043///
28044/// Subset of the full column-type grammar — we deliberately
28045/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
28046/// here because function-arg types in v7.12.4 are mostly the
28047/// bare form (`text`, `int`, `bytea`, …).
28048/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
28049/// than being `name TYPE`?
28050///
28051/// The multi-word spellings SQL allows for a bare argument type, each
28052/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
28053///
28054/// NOTE this list also exists in `spg-storage`, which computes the
28055/// signature key from the rendered argument text and has to reach the
28056/// same verdict. The two crates are siblings — neither depends on the
28057/// other — and each already carries its own table of type spellings
28058/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
28059/// there), so this follows the structure rather than inventing new
28060/// duplication. Recorded as V49.
28061pub fn is_multiword_type_phrase(phrase: &str) -> bool {
28062 let t = phrase.trim().to_ascii_lowercase();
28063 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
28064 matches!(
28065 base,
28066 "double precision"
28067 | "character varying"
28068 | "bit varying"
28069 | "timestamp with time zone"
28070 | "timestamp without time zone"
28071 | "time with time zone"
28072 | "time without time zone"
28073 | "national character"
28074 | "national character varying"
28075 )
28076}
28077
28078fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
28079 Some(match ident.to_ascii_lowercase().as_str() {
28080 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
28081 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
28082 "bigint" => ColumnTypeName::BigInt,
28083 "float" | "double" => ColumnTypeName::Float,
28084 // v7.39 (round 269) — real is 32-bit.
28085 "real" | "float4" => ColumnTypeName::Real,
28086 "text" => ColumnTypeName::Text,
28087 "bool" | "boolean" => ColumnTypeName::Bool,
28088 "date" => ColumnTypeName::Date,
28089 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
28090 "timestamptz" => ColumnTypeName::Timestamptz,
28091 "json" => ColumnTypeName::Json,
28092 "jsonb" => ColumnTypeName::Jsonb,
28093 "bytea" | "bytes" => ColumnTypeName::Bytes,
28094 "tsvector" => ColumnTypeName::TsVector,
28095 "tsquery" => ColumnTypeName::TsQuery,
28096 "uuid" => ColumnTypeName::Uuid,
28097 "interval" => ColumnTypeName::Interval,
28098 "time" => ColumnTypeName::Time,
28099 "year" => ColumnTypeName::Year,
28100 "timetz" => ColumnTypeName::TimeTz,
28101 "money" => ColumnTypeName::Money,
28102 _ => return None,
28103 })
28104}
28105
28106/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
28107/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
28108///
28109/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28110/// / embedded SQL land in v7.12.5+):
28111///
28112/// ```text
28113/// body := [ws] block [ws]
28114/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28115/// stmt := assign | return
28116/// assign := assign_target := expr
28117/// assign_target := ( NEW | OLD ) . ident | ident
28118/// return := RETURN ( NEW | OLD | NULL | expr )
28119/// ```
28120///
28121/// `expr` is parsed by recursing into the regular `Parser` — so a
28122/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28123/// NEW.subject || ' ' || NEW.sender)` body shape works without
28124/// the body parser knowing what `to_tsvector` is.
28125///
28126/// Errors here cause the caller to fall back to
28127/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28128/// successful, but the executor will refuse to invoke the
28129/// function with an "unparseable body" error.
28130/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28131/// from the crate root as `spg_sql::parse_function_body`.
28132pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28133 parse_plpgsql_body(body)
28134}
28135
28136fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28137 // Use the regular lexer on the body text. The trailing
28138 // `END;` may or may not have a semicolon; the lexer treats
28139 // both forms identically.
28140 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28141 message: alloc::format!("plpgsql body lex error: {e}"),
28142 token_pos: 0,
28143 })?;
28144 let mut parser = Parser::new(tokens);
28145 parser.parse_plpgsql_block()
28146}
28147
28148/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28149fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28150 match v {
28151 crate::ast::SetValue::String(s)
28152 | crate::ast::SetValue::Ident(s)
28153 | crate::ast::SetValue::Number(s) => s.clone(),
28154 crate::ast::SetValue::Default => "DEFAULT".into(),
28155 crate::ast::SetValue::Null => "NULL".into(),
28156 }
28157}
28158
28159/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28160/// contains an aggregate call at ITS OWN query level (recursion stops at
28161/// sublink boundaries — a sublink's aggregates belong to the sublink).
28162/// Backs the "aggregate functions are not allowed in a recursive query's
28163/// recursive term" well-formedness check.
28164/// v7.40.0 — is this the name of an aggregate? The same list
28165/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28166/// rewrite can leave an aggregate's ARGUMENT alone.
28167pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28168 AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28169}
28170
28171const AGG_NAMES: &[&str] = &[
28172 "count",
28173 "sum",
28174 "min",
28175 "max",
28176 "avg",
28177 "string_agg",
28178 "array_agg",
28179 "bool_and",
28180 "bool_or",
28181 "every",
28182 "any_value",
28183 "json_agg",
28184 "jsonb_agg",
28185 "json_object_agg",
28186 "jsonb_object_agg",
28187 "bit_and",
28188 "bit_or",
28189 "bit_xor",
28190 "var_pop",
28191 "var_samp",
28192 "variance",
28193 "std",
28194 "stddev",
28195 "stddev_pop",
28196 "stddev_samp",
28197 "range_agg",
28198 "range_intersect_agg",
28199 "percentile_cont",
28200 "percentile_disc",
28201 "mode",
28202 "corr",
28203 "covar_pop",
28204 "covar_samp",
28205];
28206
28207fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28208 match e {
28209 Expr::AggregateOrdered { .. } => true,
28210 Expr::FunctionCall { name, args } => {
28211 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28212 || args.iter().any(expr_has_toplevel_aggregate)
28213 }
28214 Expr::NamedArg { expr, .. }
28215 | Expr::Variadic(expr)
28216 | Expr::Unary { expr, .. }
28217 | Expr::Cast { expr, .. }
28218 | Expr::IsNull { expr, .. }
28219 | Expr::FieldAccess { base: expr, .. }
28220 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28221 Expr::Binary { lhs, rhs, .. } => {
28222 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28223 }
28224 Expr::Like { expr, pattern, .. } => {
28225 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28226 }
28227 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28228 Expr::InList { expr, list, .. } => {
28229 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28230 }
28231 Expr::ArraySubscript { target, index } => {
28232 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28233 }
28234 Expr::ArraySlice { target, lo, hi } => {
28235 expr_has_toplevel_aggregate(target)
28236 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28237 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28238 }
28239 Expr::AnyAll { expr, array, .. } => {
28240 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28241 }
28242 Expr::Case {
28243 operand,
28244 branches,
28245 else_branch,
28246 } => {
28247 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28248 || branches
28249 .iter()
28250 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28251 || else_branch
28252 .as_deref()
28253 .is_some_and(expr_has_toplevel_aggregate)
28254 }
28255 // The outer-level operands of a sublink can aggregate; the sublink's
28256 // own body cannot leak its aggregates up here.
28257 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28258 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28259 row.iter().any(expr_has_toplevel_aggregate)
28260 }
28261 _ => false,
28262 }
28263}
28264
28265/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28266/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28267/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28268/// sublink and is legal in a recursive term, so it is not walked here.
28269fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28270 let mut exprs: Vec<&Expr> = Vec::new();
28271 for it in &s.items {
28272 if let crate::ast::SelectItem::Expr { expr, .. } = it {
28273 exprs.push(expr);
28274 }
28275 }
28276 if let Some(w) = &s.where_ {
28277 exprs.push(w);
28278 }
28279 if let Some(h) = &s.having {
28280 exprs.push(h);
28281 }
28282 if let Some(g) = &s.group_by {
28283 exprs.extend(g.iter());
28284 }
28285 if let Some(from) = &s.from {
28286 for j in &from.joins {
28287 if let Some(on) = &j.on {
28288 exprs.push(on);
28289 }
28290 }
28291 }
28292 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28293}
28294
28295/// Does this expression contain a sublink whose subquery mentions `name`?
28296fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28297 match e {
28298 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28299 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28300 Expr::InSubquery { expr, subquery, .. } => {
28301 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28302 }
28303 Expr::RowInSubquery { row, subquery, .. } => {
28304 row.iter().any(|x| expr_sublink_mentions(x, name))
28305 || select_mentions_table(subquery, name)
28306 }
28307 Expr::RowCmpSubquery { row, subquery, .. } => {
28308 row.iter().any(|x| expr_sublink_mentions(x, name))
28309 || select_mentions_table(subquery, name)
28310 }
28311 Expr::NamedArg { expr, .. }
28312 | Expr::Variadic(expr)
28313 | Expr::Unary { expr, .. }
28314 | Expr::Cast { expr, .. }
28315 | Expr::IsNull { expr, .. }
28316 | Expr::FieldAccess { base: expr, .. }
28317 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28318 Expr::Binary { lhs, rhs, .. } => {
28319 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28320 }
28321 Expr::Like { expr, pattern, .. } => {
28322 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28323 }
28324 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28325 args.iter().any(|x| expr_sublink_mentions(x, name))
28326 }
28327 Expr::InList { expr, list, .. } => {
28328 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28329 }
28330 Expr::ArraySubscript { target, index } => {
28331 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28332 }
28333 Expr::ArraySlice { target, lo, hi } => {
28334 expr_sublink_mentions(target, name)
28335 || lo
28336 .as_deref()
28337 .is_some_and(|x| expr_sublink_mentions(x, name))
28338 || hi
28339 .as_deref()
28340 .is_some_and(|x| expr_sublink_mentions(x, name))
28341 }
28342 Expr::AnyAll { expr, array, .. } => {
28343 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28344 }
28345 Expr::Case {
28346 operand,
28347 branches,
28348 else_branch,
28349 } => {
28350 operand
28351 .as_deref()
28352 .is_some_and(|x| expr_sublink_mentions(x, name))
28353 || branches
28354 .iter()
28355 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28356 || else_branch
28357 .as_deref()
28358 .is_some_and(|x| expr_sublink_mentions(x, name))
28359 }
28360 _ => false,
28361 }
28362}
28363
28364/// Does this SELECT (in full — FROM tables, derived tables, its own
28365/// sublinks, and union arms) mention the named table?
28366fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28367 if let Some(from) = &s.from {
28368 if from.primary.name.eq_ignore_ascii_case(name) {
28369 return true;
28370 }
28371 if let Some(sub) = &from.primary.lateral_subquery
28372 && select_mentions_table(sub, name)
28373 {
28374 return true;
28375 }
28376 for j in &from.joins {
28377 if j.table.name.eq_ignore_ascii_case(name) {
28378 return true;
28379 }
28380 if let Some(sub) = &j.table.lateral_subquery
28381 && select_mentions_table(sub, name)
28382 {
28383 return true;
28384 }
28385 }
28386 }
28387 if select_has_self_ref_in_sublink(s, name) {
28388 return true;
28389 }
28390 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28391}
28392
28393/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28394/// row count, the way PG evaluates one before applying it.
28395///
28396/// `None` = not a constant (a column, a subquery, a function call).
28397/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28398/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28399/// All wordings were read off live PG 18.4.
28400fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28401 use crate::ast::{BinOp, Expr, Literal, UnOp};
28402 match e {
28403 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28404 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28405 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28406 }
28407 // PG coerces a string by its CONTENT, and fails on the value.
28408 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28409 |_| {
28410 Err(alloc::format!(
28411 "invalid input syntax for type bigint: \"{t}\""
28412 ))
28413 },
28414 |n| Ok(i128::from(n)),
28415 )),
28416 Expr::Literal(Literal::Bool(_)) => Some(Err(
28417 "argument of {L} must be type bigint, not type boolean".into(),
28418 )),
28419 Expr::Unary {
28420 op: UnOp::Neg,
28421 expr,
28422 } => match fold_limit_constant(expr)? {
28423 Ok(v) => Some(Ok(-v)),
28424 e @ Err(_) => Some(e),
28425 },
28426 Expr::Binary { lhs, op, rhs } => {
28427 let a = match fold_limit_constant(lhs)? {
28428 Ok(v) => v,
28429 e @ Err(_) => return Some(e),
28430 };
28431 let b = match fold_limit_constant(rhs)? {
28432 Ok(v) => v,
28433 e @ Err(_) => return Some(e),
28434 };
28435 let out = match op {
28436 BinOp::Add => a.checked_add(b),
28437 BinOp::Sub => a.checked_sub(b),
28438 BinOp::Mul => a.checked_mul(b),
28439 BinOp::Div if b != 0 => a.checked_div(b),
28440 BinOp::Div => return Some(Err("division by zero".into())),
28441 BinOp::Mod if b != 0 => a.checked_rem(b),
28442 BinOp::Mod => return Some(Err("division by zero".into())),
28443 _ => return None,
28444 };
28445 // PG evaluates the arithmetic in the operand's own type, so an
28446 // int-by-int product that leaves int range fails there — before
28447 // the row count is ever looked at.
28448 match out {
28449 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28450 Some(Err("integer out of range".into()))
28451 }
28452 Some(v) => Some(Ok(v)),
28453 None => Some(Err("integer out of range".into())),
28454 }
28455 }
28456 _ => None,
28457 }
28458}
28459
28460/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28461/// cast, which is what makes `LIMIT 2.5` keep three rows.
28462fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28463 if scale == 0 {
28464 return unscaled;
28465 }
28466 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28467 return 0;
28468 };
28469 let neg = unscaled < 0;
28470 let mag = unscaled.unsigned_abs() as i128;
28471 let rounded = (mag + div / 2) / div;
28472 if neg { -rounded } else { rounded }
28473}
28474
28475#[cfg(test)]
28476mod tests {
28477 use super::*;
28478 use alloc::string::ToString;
28479
28480 fn parse(s: &str) -> Statement {
28481 parse_statement(s).expect("parse ok")
28482 }
28483
28484 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28485 // `tables`, `partition`, etc. are unreserved keywords per PG's
28486 // `pg_get_keywords()` and MUST be usable as column / table /
28487 // alias names. Pre-T4 every drop-in user whose schema had one
28488 // of these as a column name (sentori events.release, mailrs
28489 // messages.index in some forks) blew the parser up at CREATE
28490 // TABLE time with "expected identifier, got Release". The
28491 // generalisation lives in `unreserved_keyword_text` + the
28492 // `expect_ident_like` and `parse_atom` arms that consult it.
28493 #[test]
28494 fn release_usable_as_column_name_in_create_table() {
28495 let stmt =
28496 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28497 if let Statement::CreateTable(t) = stmt {
28498 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28499 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28500 } else {
28501 panic!("expected CreateTable");
28502 }
28503 }
28504
28505 #[test]
28506 fn release_usable_as_column_ref_in_select_projection() {
28507 // The sentori `0003_partition_events.sql` INSERT-SELECT
28508 // walk references `release` in both column lists; the
28509 // projection-side use exercises `parse_atom`'s relaxed
28510 // identifier set.
28511 parse("SELECT id, release, payload FROM events WHERE id = 1");
28512 }
28513
28514 #[test]
28515 fn release_usable_as_column_ref_in_insert_column_list() {
28516 // INSERT INTO t (id, release, payload) VALUES (…)
28517 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28518 }
28519
28520 #[test]
28521 fn alter_column_drop_not_null_uses_keyword_drop_token() {
28522 // Sentori `0013_audit_tombstone.sql` issues
28523 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28524 // emits Token::Drop (not Ident("drop")); the parser must
28525 // accept both in the ALTER COLUMN sub-dispatch.
28526 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28527 }
28528
28529 #[test]
28530 fn create_index_accepts_parenthesised_expression_key() {
28531 // sentori `0040_events_bundle_idx.sql` shape — JSONB
28532 // expression index. Pre-T4 the parser bailed at the
28533 // inner `(` with "expected column ident or expression,
28534 // got LParen". The Token::LParen arm in CREATE INDEX
28535 // routes through the expression parser instead.
28536 parse(
28537 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28538 ON events ((payload->'bundle'->>'id'))",
28539 );
28540 }
28541
28542 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28543 // surface as parse errors, never stack overflows (embed hosts
28544 // abort on overflow).
28545 /// The nesting budget is a COUNT; what it has to fit inside is a
28546 /// number of BYTES, and only one of those two is stable across
28547 /// compiler versions. Round 847 measured 30,336 bytes per level
28548 /// after a toolchain move, which puts 64 levels at 1.94 MB and
28549 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28550 /// aborted instead of erroring, which is precisely the outcome it
28551 /// exists to rule out.
28552 ///
28553 /// So the budget is metered rather than assumed. The ceiling leaves
28554 /// the depth SPG advertises fitting in a default 2 MiB thread with
28555 /// room to spare, in the debug build, where frames are widest.
28556 #[test]
28557 fn nesting_frame_cost_stays_under_ceiling() {
28558 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28559 // thread keeps a margin for whatever called the parser.
28560 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28561
28562 frame_meter::reset();
28563 let depth = frame_meter::SAMPLE_HI + 8;
28564 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28565 parse(&sql);
28566
28567 let per_level = frame_meter::bytes_per_level();
28568 {
28569 extern crate std;
28570 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28571 }
28572 assert!(
28573 per_level <= CEILING,
28574 "{per_level} bytes per nesting level exceeds {CEILING}; \
28575 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28576 in parse_expr_inner / parse_unary rather than lowering the \
28577 depth or widening the stack.",
28578 per_level * MAX_NEST_DEPTH
28579 );
28580 }
28581
28582 #[test]
28583 fn nesting_budget_errors_cleanly() {
28584 let depth = MAX_NEST_DEPTH + 50;
28585 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28586 let err = parse_statement(&sql).expect_err("must reject");
28587 assert!(err.message.contains("nests deeper"), "{err:?}");
28588 // Within budget still parses.
28589 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28590 parse(&sql);
28591 }
28592
28593 #[test]
28594 fn binary_chain_budget_errors_cleanly() {
28595 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28596 let err = parse_statement(&sql).expect_err("must reject");
28597 assert!(err.message.contains("chained binary"), "{err:?}");
28598 // Within budget still parses (chain depth ≤ budget is safe
28599 // for recursive eval/drop on 2 MiB stacks).
28600 let sql = format!("SELECT 1{}", " + 1".repeat(200));
28601 parse(&sql);
28602 }
28603
28604 #[test]
28605 fn in_list_unaffected_by_chain_budget() {
28606 // Flat InList: 20k elements parse fine and stay flat.
28607 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28608 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28609 let Statement::Select(s) = parse(&sql) else {
28610 panic!("expected select")
28611 };
28612 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28613 panic!("expected flat InList, got {:?}", s.where_)
28614 };
28615 assert_eq!(list.len(), 20_000);
28616 assert!(!negated);
28617 }
28618
28619 fn lit_int(n: i64) -> Expr {
28620 Expr::Literal(Literal::Integer(n))
28621 }
28622
28623 fn col(name: &str) -> Expr {
28624 Expr::Column(ColumnName {
28625 qualifier: None,
28626 name: name.into(),
28627 })
28628 }
28629
28630 #[test]
28631 fn select_single_integer() {
28632 let s = parse("SELECT 1");
28633 let Statement::Select(s) = s else {
28634 panic!("expected SELECT")
28635 };
28636 assert_eq!(s.items.len(), 1);
28637 assert!(s.from.is_none());
28638 assert!(s.where_.is_none());
28639 }
28640
28641 #[test]
28642 fn select_multiple_literal_kinds() {
28643 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28644 let Statement::Select(s) = s else {
28645 panic!("expected SELECT")
28646 };
28647 assert_eq!(s.items.len(), 5);
28648 }
28649
28650 #[test]
28651 fn select_wildcard_from_table() {
28652 let s = parse("SELECT * FROM users");
28653 let Statement::Select(s) = s else {
28654 panic!("expected SELECT")
28655 };
28656 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28657 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28658 }
28659
28660 #[test]
28661 fn select_with_table_alias() {
28662 let s = parse("SELECT * FROM users AS u");
28663 let Statement::Select(s) = s else {
28664 panic!("expected SELECT")
28665 };
28666 let t = &s.from.as_ref().unwrap().primary;
28667 assert_eq!(t.name, "users");
28668 assert_eq!(t.alias.as_deref(), Some("u"));
28669 }
28670
28671 #[test]
28672 fn select_with_where_eq() {
28673 let s = parse("SELECT a FROM t WHERE a = 1");
28674 let Statement::Select(s) = s else {
28675 panic!("expected SELECT")
28676 };
28677 let w = s.where_.unwrap();
28678 assert_eq!(
28679 w,
28680 Expr::Binary {
28681 lhs: Box::new(col("a")),
28682 op: BinOp::Eq,
28683 rhs: Box::new(lit_int(1)),
28684 }
28685 );
28686 }
28687
28688 #[test]
28689 fn arithmetic_precedence() {
28690 let s = parse("SELECT 1 + 2 * 3");
28691 let Statement::Select(s) = s else {
28692 panic!("expected SELECT")
28693 };
28694 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28695 panic!("wildcard?")
28696 };
28697 assert_eq!(
28698 expr,
28699 &Expr::Binary {
28700 lhs: Box::new(lit_int(1)),
28701 op: BinOp::Add,
28702 rhs: Box::new(Expr::Binary {
28703 lhs: Box::new(lit_int(2)),
28704 op: BinOp::Mul,
28705 rhs: Box::new(lit_int(3)),
28706 }),
28707 }
28708 );
28709 }
28710
28711 #[test]
28712 fn parentheses_override_precedence() {
28713 let s = parse("SELECT (1 + 2) * 3");
28714 let Statement::Select(s) = s else {
28715 panic!("expected SELECT")
28716 };
28717 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28718 panic!()
28719 };
28720 assert_eq!(
28721 expr,
28722 &Expr::Binary {
28723 lhs: Box::new(Expr::Binary {
28724 lhs: Box::new(lit_int(1)),
28725 op: BinOp::Add,
28726 rhs: Box::new(lit_int(2)),
28727 }),
28728 op: BinOp::Mul,
28729 rhs: Box::new(lit_int(3)),
28730 }
28731 );
28732 }
28733
28734 #[test]
28735 fn not_binds_below_comparison() {
28736 // `NOT a = 1` should parse as `NOT (a = 1)`.
28737 let s = parse("SELECT NOT a = 1 FROM t");
28738 let Statement::Select(s) = s else {
28739 panic!("expected SELECT")
28740 };
28741 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28742 panic!()
28743 };
28744 assert_eq!(
28745 expr,
28746 &Expr::Unary {
28747 op: UnOp::Not,
28748 expr: Box::new(Expr::Binary {
28749 lhs: Box::new(col("a")),
28750 op: BinOp::Eq,
28751 rhs: Box::new(lit_int(1)),
28752 }),
28753 }
28754 );
28755 }
28756
28757 #[test]
28758 fn unary_minus_binds_above_multiplication() {
28759 // `-a * 2` should be `(-a) * 2`.
28760 let s = parse("SELECT -a * 2 FROM t");
28761 let Statement::Select(s) = s else {
28762 panic!("expected SELECT")
28763 };
28764 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28765 panic!()
28766 };
28767 assert_eq!(
28768 expr,
28769 &Expr::Binary {
28770 lhs: Box::new(Expr::Unary {
28771 op: UnOp::Neg,
28772 expr: Box::new(col("a")),
28773 }),
28774 op: BinOp::Mul,
28775 rhs: Box::new(lit_int(2)),
28776 }
28777 );
28778 }
28779
28780 #[test]
28781 fn qualified_column() {
28782 let s = parse("SELECT t.col FROM t");
28783 let Statement::Select(s) = s else {
28784 panic!("expected SELECT")
28785 };
28786 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28787 panic!()
28788 };
28789 assert_eq!(
28790 expr,
28791 &Expr::Column(ColumnName {
28792 qualifier: Some("t".into()),
28793 name: "col".into()
28794 })
28795 );
28796 }
28797
28798 #[test]
28799 fn select_item_alias_with_as() {
28800 let s = parse("SELECT a AS y FROM t");
28801 let Statement::Select(s) = s else {
28802 panic!("expected SELECT")
28803 };
28804 let SelectItem::Expr { alias, .. } = &s.items[0] else {
28805 panic!()
28806 };
28807 assert_eq!(alias.as_deref(), Some("y"));
28808 }
28809
28810 #[test]
28811 fn trailing_semicolon_accepted() {
28812 let s = parse("SELECT 1;");
28813 let Statement::Select(s) = s else {
28814 panic!("expected SELECT")
28815 };
28816 assert_eq!(s.items.len(), 1);
28817 }
28818
28819 #[test]
28820 fn boolean_chain_with_and_or_not() {
28821 // (NOT a) OR (b AND (NOT c))
28822 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28823 let Statement::Select(s) = s else {
28824 panic!("expected SELECT")
28825 };
28826 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28827 panic!()
28828 };
28829 let expected = Expr::Binary {
28830 lhs: Box::new(Expr::Unary {
28831 op: UnOp::Not,
28832 expr: Box::new(col("a")),
28833 }),
28834 op: BinOp::Or,
28835 rhs: Box::new(Expr::Binary {
28836 lhs: Box::new(col("b")),
28837 op: BinOp::And,
28838 rhs: Box::new(Expr::Unary {
28839 op: UnOp::Not,
28840 expr: Box::new(col("c")),
28841 }),
28842 }),
28843 };
28844 assert_eq!(expr, &expected);
28845 }
28846
28847 #[test]
28848 fn empty_input_errors() {
28849 // v7.14.0 — pg_dump preambles emit several comment-only
28850 // / blank-line statements that collapse to Statement::
28851 // Empty rather than a parse error. The old "SELECT in
28852 // message" assertion is stale; verify the new contract:
28853 // empty / whitespace / comment-only input parses to
28854 // Statement::Empty.
28855 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28856 assert!(matches!(
28857 parse_statement(" \n\t ").unwrap(),
28858 Statement::Empty
28859 ));
28860 // Sanity: malformed-but-non-empty still errors.
28861 assert!(parse_statement("SELECT FROM WHERE").is_err());
28862 }
28863
28864 #[test]
28865 fn unmatched_paren_errors() {
28866 assert!(parse_statement("SELECT (1 + 2").is_err());
28867 }
28868
28869 #[test]
28870 fn display_round_trip_simple_select() {
28871 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28872 let text = original.to_string();
28873 let again = parse_statement(&text).expect("re-parse");
28874 assert_eq!(original, again);
28875 }
28876
28877 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28878
28879 #[test]
28880 fn create_table_single_column() {
28881 let s = parse("CREATE TABLE foo (a INT)");
28882 let Statement::CreateTable(c) = s else {
28883 panic!("expected CreateTable")
28884 };
28885 assert_eq!(c.name, "foo");
28886 assert_eq!(c.columns.len(), 1);
28887 assert_eq!(c.columns[0].name, "a");
28888 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28889 assert!(c.columns[0].nullable);
28890 }
28891
28892 #[test]
28893 fn create_table_multi_column_with_not_null_mix() {
28894 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28895 let Statement::CreateTable(c) = s else {
28896 panic!()
28897 };
28898 assert_eq!(c.columns.len(), 4);
28899 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28900 assert!(!c.columns[0].nullable);
28901 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28902 assert!(c.columns[1].nullable);
28903 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28904 assert!(!c.columns[2].nullable);
28905 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28906 }
28907
28908 #[test]
28909 fn create_table_bigint_supported() {
28910 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28911 let Statement::CreateTable(c) = s else {
28912 panic!()
28913 };
28914 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28915 }
28916
28917 #[test]
28918 fn create_table_vector_default_is_f32() {
28919 let s = parse("CREATE TABLE t (v VECTOR(128))");
28920 let Statement::CreateTable(c) = s else {
28921 panic!()
28922 };
28923 assert_eq!(
28924 c.columns[0].ty,
28925 ColumnTypeName::Vector {
28926 dim: 128,
28927 encoding: VecEncoding::F32,
28928 },
28929 );
28930 }
28931
28932 #[test]
28933 fn create_table_vector_using_sq8() {
28934 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28935 // Case-insensitive on both `USING` and the encoding name.
28936 for sql in [
28937 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28938 "CREATE TABLE t (v VECTOR(128) using sq8)",
28939 ] {
28940 let s = parse(sql);
28941 let Statement::CreateTable(c) = s else {
28942 panic!()
28943 };
28944 assert_eq!(
28945 c.columns[0].ty,
28946 ColumnTypeName::Vector {
28947 dim: 128,
28948 encoding: VecEncoding::Sq8,
28949 },
28950 "{sql}",
28951 );
28952 }
28953 }
28954
28955 #[test]
28956 fn create_table_vector_using_unknown_errors() {
28957 // v7.16.1 — the inline `USING <encoding>` shape on
28958 // CREATE TABLE column defs was withdrawn before
28959 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28960 // (col vector_<metric>_ops)`; the parser now rejects
28961 // USING at column-list position with a clearer
28962 // "expected ',' or ')'" message. Test asserts the
28963 // current rejection, not the old "unknown vector
28964 // encoding" string.
28965 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28966 assert!(
28967 err.message.contains("USING")
28968 || err.message.contains("using")
28969 || err.message.contains("')'")
28970 || err.message.contains("','"),
28971 "expected USING/column-list rejection, got: {}",
28972 err.message
28973 );
28974 }
28975
28976 #[test]
28977 fn vector_using_sq8_display_roundtrips() {
28978 // The Display impl must produce text that re-parses to the
28979 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28980 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28981 let Statement::CreateTable(c) = s else {
28982 panic!()
28983 };
28984 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28985 }
28986
28987 #[test]
28988 fn parser_recognises_placeholders() {
28989 use crate::ast::{Expr, SelectItem, Statement};
28990 // $N in expression position parses as Expr::Placeholder(N).
28991 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28992 let Statement::Select(sel) = s else { panic!() };
28993 assert!(matches!(
28994 sel.items[0],
28995 SelectItem::Expr {
28996 expr: Expr::Placeholder(1),
28997 alias: None
28998 }
28999 ));
29000 // $2 + 1
29001 let SelectItem::Expr {
29002 expr: Expr::Binary { lhs, rhs, .. },
29003 ..
29004 } = &sel.items[1]
29005 else {
29006 panic!()
29007 };
29008 assert!(matches!(**lhs, Expr::Placeholder(2)));
29009 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
29010 // WHERE x = $3
29011 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
29012 panic!()
29013 };
29014 assert!(matches!(**rhs, Expr::Placeholder(3)));
29015 }
29016
29017 #[test]
29018 fn parser_rejects_dollar_zero() {
29019 // $0 is not valid in PG; the lexer rejects it.
29020 assert!(parse_statement("SELECT $0").is_err());
29021 }
29022
29023 #[test]
29024 fn placeholder_display_roundtrips() {
29025 // The Display impl must produce text that re-lexes to the
29026 // same Placeholder token.
29027 let s = parse("SELECT $42 FROM t");
29028 let printed = s.to_string();
29029 assert!(printed.contains("$42"));
29030 let again = parse(&printed);
29031 assert_eq!(s, again);
29032 }
29033
29034 #[test]
29035 fn alter_index_rebuild_bare() {
29036 use crate::ast::{AlterIndexTarget, Statement};
29037 let s = parse("ALTER INDEX my_idx REBUILD");
29038 let Statement::AlterIndex(a) = s else {
29039 panic!("expected AlterIndex, got {s:?}")
29040 };
29041 assert_eq!(a.name, "my_idx");
29042 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
29043 }
29044
29045 #[test]
29046 fn alter_index_rebuild_with_encoding() {
29047 use crate::ast::{AlterIndexTarget, Statement};
29048 for (sql, want) in [
29049 (
29050 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
29051 VecEncoding::F32,
29052 ),
29053 (
29054 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
29055 VecEncoding::Sq8,
29056 ),
29057 (
29058 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29059 VecEncoding::F16,
29060 ),
29061 ] {
29062 let s = parse(sql);
29063 let Statement::AlterIndex(a) = s else {
29064 panic!("{sql}: expected AlterIndex")
29065 };
29066 assert_eq!(a.name, "my_idx");
29067 assert_eq!(
29068 a.target,
29069 AlterIndexTarget::Rebuild {
29070 encoding: Some(want)
29071 },
29072 "{sql}"
29073 );
29074 }
29075 }
29076
29077 #[test]
29078 fn alter_index_rebuild_unknown_encoding_errors() {
29079 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
29080 assert!(
29081 err.message.contains("unknown vector encoding"),
29082 "got: {}",
29083 err.message
29084 );
29085 }
29086
29087 #[test]
29088 fn alter_index_rebuild_display_roundtrips() {
29089 for (input, want) in [
29090 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
29091 (
29092 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29093 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29094 ),
29095 (
29096 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29097 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29098 ),
29099 ] {
29100 let s = parse(input);
29101 assert_eq!(s.to_string(), want);
29102 }
29103 }
29104
29105 #[test]
29106 fn create_table_unknown_type_defers_to_engine() {
29107 // v4.9 picked XML as a parse-time "unsupported column
29108 // type" probe. v7.17.0 Phase 1.4 changed the contract:
29109 // an unknown type ident parses as Text + `user_type_ref`
29110 // so CREATE TABLE can resolve user-defined enum / domain
29111 // types — rejection of truly-unknown types moved to the
29112 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29113 // to a first-class built-in, so this probe switched to a
29114 // synthetic name nothing in the lexer will ever recognise.
29115 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29116 let Statement::CreateTable(t) = stmt else {
29117 panic!("expected CreateTable");
29118 };
29119 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29120 }
29121
29122 #[test]
29123 fn create_table_missing_table_keyword_errors() {
29124 assert!(parse_statement("CREATE x (a INT)").is_err());
29125 }
29126
29127 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29128 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29129
29130 #[test]
29131 fn parse_create_table_partition_by_range() {
29132 use crate::ast::{PartitionBySpec, PartitionKindAst};
29133 let stmt = parse_statement(
29134 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29135 payload JSONB) PARTITION BY RANGE (ts)",
29136 )
29137 .unwrap();
29138 let Statement::CreateTable(t) = stmt else {
29139 panic!("expected CreateTable");
29140 };
29141 assert!(t.partition_of.is_none(), "parent has no partition_of");
29142 assert_eq!(t.columns.len(), 3);
29143 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29144 assert_eq!(
29145 by,
29146 &PartitionBySpec {
29147 kind: PartitionKindAst::Range,
29148 key_columns: alloc::vec!["ts".to_string()],
29149 }
29150 );
29151 // Display round-trip preserves the suffix. `quote_ident`
29152 // only adds double quotes when the ident needs escaping, so
29153 // a plain `ts` survives bare here.
29154 assert!(
29155 t.to_string().contains("PARTITION BY RANGE (ts)"),
29156 "Display lost PARTITION BY suffix: {t}"
29157 );
29158 }
29159
29160 #[test]
29161 fn parse_create_table_partition_of_range() {
29162 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29163 let stmt = parse_statement(
29164 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29165 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29166 )
29167 .unwrap();
29168 let Statement::CreateTable(t) = stmt else {
29169 panic!("expected CreateTable");
29170 };
29171 assert!(t.columns.is_empty(), "child inherits columns from parent");
29172 assert!(t.partition_by.is_none());
29173 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29174 assert_eq!(of.parent_name, "events_partitioned");
29175 let PartitionOfSpec { bounds, .. } = of.clone();
29176 match bounds {
29177 PartitionOfBoundsAst::Range { lower, upper } => {
29178 assert!(lower.to_string().contains("2026-06-01"));
29179 assert!(upper.to_string().contains("2026-07-01"));
29180 }
29181 other => panic!("expected Range, got {other:?}"),
29182 }
29183 // Display round-trip emits the FOR VALUES tail. `quote_ident`
29184 // skips quotes when not required, so the parent name appears
29185 // bare here.
29186 let s = t.to_string();
29187 assert!(
29188 s.contains("PARTITION OF events_partitioned"),
29189 "Display lost PARTITION OF: {s}"
29190 );
29191 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29192 assert!(s.contains(") TO ("), "Display lost TO: {s}");
29193 }
29194
29195 #[test]
29196 fn parse_create_table_partition_of_default() {
29197 use crate::ast::PartitionOfBoundsAst;
29198 let stmt =
29199 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29200 .unwrap();
29201 let Statement::CreateTable(t) = stmt else {
29202 panic!("expected CreateTable");
29203 };
29204 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29205 assert_eq!(of.parent_name, "events_partitioned");
29206 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29207 assert!(
29208 t.to_string()
29209 .contains("PARTITION OF events_partitioned DEFAULT"),
29210 "Display lost DEFAULT: {t}"
29211 );
29212 }
29213
29214 #[test]
29215 fn parse_create_table_partition_by_list() {
29216 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29217 // child with `FOR VALUES IN (lit, lit, …)`.
29218 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29219 let parent =
29220 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29221 .unwrap();
29222 let Statement::CreateTable(t) = parent else {
29223 panic!("expected CreateTable");
29224 };
29225 let Some(PartitionBySpec {
29226 kind,
29227 ref key_columns,
29228 }) = t.partition_by
29229 else {
29230 panic!("expected PARTITION BY");
29231 };
29232 assert_eq!(kind, PartitionKindAst::List);
29233 assert_eq!(*key_columns, vec!["region".to_string()]);
29234 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29235
29236 let child = parse_statement(
29237 "CREATE TABLE events_apac PARTITION OF events_listed \
29238 FOR VALUES IN ('jp', 'kr', 'tw')",
29239 )
29240 .unwrap();
29241 let Statement::CreateTable(c) = child else {
29242 panic!("expected CreateTable");
29243 };
29244 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29245 let PartitionOfBoundsAst::List { values } = &of.bounds else {
29246 panic!("expected List bounds, got {:?}", of.bounds);
29247 };
29248 assert_eq!(values.len(), 3);
29249 let disp = c.to_string();
29250 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29251 }
29252
29253 #[test]
29254 fn parse_create_table_partition_by_hash() {
29255 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29256 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29257 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29258 let parent =
29259 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29260 let Statement::CreateTable(t) = parent else {
29261 panic!("expected CreateTable");
29262 };
29263 let Some(PartitionBySpec {
29264 kind,
29265 ref key_columns,
29266 }) = t.partition_by
29267 else {
29268 panic!("expected PARTITION BY");
29269 };
29270 assert_eq!(kind, PartitionKindAst::Hash);
29271 assert_eq!(*key_columns, vec!["id".to_string()]);
29272 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29273
29274 let child = parse_statement(
29275 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29276 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29277 )
29278 .unwrap();
29279 let Statement::CreateTable(c) = child else {
29280 panic!("expected CreateTable");
29281 };
29282 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29283 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29284 panic!("expected Hash bounds");
29285 };
29286 assert_eq!(modulus, 4);
29287 assert_eq!(remainder, 0);
29288 let disp = c.to_string();
29289 assert!(
29290 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29291 "Display lost HASH bounds: {disp}"
29292 );
29293
29294 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29295 let bad = parse_statement(
29296 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29297 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29298 );
29299 let msg = format!("{}", bad.unwrap_err());
29300 assert!(
29301 msg.contains("REMAINDER") && msg.contains("MODULUS"),
29302 "expected REMAINDER/MODULUS validation error: {msg}"
29303 );
29304 }
29305
29306 #[test]
29307 fn parse_create_table_partition_of_rejects_columns() {
29308 // v7.37.6-B contract: PARTITION OF children inherit columns
29309 // from the parent; an explicit list MUST surface as a parse
29310 // error rather than getting silently ignored.
29311 let err = parse_statement(
29312 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29313 FOR VALUES FROM ('a') TO ('b')",
29314 );
29315 assert!(err.is_err(), "expected parse error for explicit columns");
29316 let msg = format!("{}", err.unwrap_err());
29317 assert!(
29318 msg.contains("PARTITION OF") && msg.contains("column"),
29319 "error should mention PARTITION OF + columns: {msg}"
29320 );
29321 }
29322
29323 #[test]
29324 fn insert_single_value() {
29325 let s = parse("INSERT INTO foo VALUES (42)");
29326 let Statement::Insert(i) = s else {
29327 panic!("expected Insert")
29328 };
29329 assert_eq!(i.table, "foo");
29330 assert_eq!(i.rows.len(), 1);
29331 assert_eq!(i.rows[0].len(), 1);
29332 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29333 }
29334
29335 #[test]
29336 fn insert_multi_value_with_mixed_literals() {
29337 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29338 let Statement::Insert(i) = s else { panic!() };
29339 assert_eq!(i.rows.len(), 1);
29340 assert_eq!(i.rows[0].len(), 5);
29341 }
29342
29343 #[test]
29344 fn insert_missing_into_errors() {
29345 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29346 }
29347
29348 #[test]
29349 fn create_table_round_trip() {
29350 let original =
29351 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29352 let text = original.to_string();
29353 let again = parse_statement(&text).expect("re-parse");
29354 assert_eq!(original, again);
29355 }
29356
29357 #[test]
29358 fn insert_round_trip_with_negation_and_string() {
29359 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29360 let text = original.to_string();
29361 let again = parse_statement(&text).expect("re-parse");
29362 assert_eq!(original, again);
29363 }
29364
29365 #[test]
29366 fn unknown_keyword_at_statement_start_errors() {
29367 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29368 // the top-level dispatch still has no branch to take.
29369 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29370 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29371 }
29372
29373 // --- v0.8 CREATE INDEX --------------------------------------------------
29374
29375 #[test]
29376 fn create_index_basic() {
29377 let s = parse("CREATE INDEX idx_id ON users (id)");
29378 let Statement::CreateIndex(c) = s else {
29379 panic!("expected CreateIndex")
29380 };
29381 assert_eq!(c.name, "idx_id");
29382 assert_eq!(c.table, "users");
29383 assert_eq!(c.column, "id");
29384 }
29385
29386 #[test]
29387 fn create_index_missing_on_errors() {
29388 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29389 }
29390
29391 #[test]
29392 fn create_index_missing_paren_errors() {
29393 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29394 }
29395
29396 #[test]
29397 fn create_index_round_trip() {
29398 let original = parse("CREATE INDEX by_name ON users (name)");
29399 let again = parse_statement(&original.to_string()).unwrap();
29400 assert_eq!(original, again);
29401 }
29402
29403 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29404
29405 #[test]
29406 fn create_unique_index_basic() {
29407 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29408 let Statement::CreateIndex(c) = s else {
29409 panic!("expected CreateIndex");
29410 };
29411 assert!(c.is_unique);
29412 assert_eq!(c.column, "a");
29413 assert!(c.partial_predicate.is_none());
29414 }
29415
29416 #[test]
29417 fn create_unique_index_partial() {
29418 // mailrs's email_templates "one default per user" shape.
29419 let s = parse(
29420 "CREATE UNIQUE INDEX idx_email_templates_user_default \
29421 ON email_templates (user_address) WHERE is_default = true",
29422 );
29423 let Statement::CreateIndex(c) = s else {
29424 panic!("expected CreateIndex");
29425 };
29426 assert!(c.is_unique);
29427 assert_eq!(c.table, "email_templates");
29428 assert_eq!(c.column, "user_address");
29429 assert!(c.partial_predicate.is_some());
29430 }
29431
29432 #[test]
29433 fn create_unique_index_composite_with_predicate() {
29434 // mailrs's calendar_events instance: composite columns.
29435 let s = parse(
29436 "CREATE UNIQUE INDEX uq_calendar_events_instance \
29437 ON calendar_events (calendar_id, uid, recurrence_id) \
29438 WHERE recurrence_id IS NOT NULL",
29439 );
29440 let Statement::CreateIndex(c) = s else {
29441 panic!("expected CreateIndex");
29442 };
29443 assert!(c.is_unique);
29444 assert_eq!(c.column, "calendar_id");
29445 assert_eq!(
29446 c.extra_columns,
29447 vec!["uid".to_string(), "recurrence_id".to_string()]
29448 );
29449 assert!(c.partial_predicate.is_some());
29450 }
29451
29452 #[test]
29453 fn create_unique_index_using_btree_ok() {
29454 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29455 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29456 }
29457
29458 #[test]
29459 fn create_unique_index_using_hnsw_rejected() {
29460 let err =
29461 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29462 assert!(err.message.contains("UNIQUE"), "{}", err.message);
29463 }
29464
29465 #[test]
29466 fn create_unique_index_round_trip() {
29467 let original = parse(
29468 "CREATE UNIQUE INDEX uq_calendar_events_master \
29469 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29470 );
29471 let again = parse_statement(&original.to_string()).unwrap();
29472 assert_eq!(original, again);
29473 }
29474
29475 #[test]
29476 fn create_unique_without_index_errors() {
29477 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29478 // v7.39 (round 340, V56) — PG 18.4, verbatim.
29479 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29480 }
29481
29482 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29483
29484 #[test]
29485 fn create_table_bytea_column() {
29486 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29487 let Statement::CreateTable(c) = s else {
29488 panic!("expected CreateTable");
29489 };
29490 assert_eq!(c.columns.len(), 2);
29491 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29492 assert!(!c.columns[1].nullable);
29493 }
29494
29495 #[test]
29496 fn create_table_bytes_alias_column() {
29497 let s = parse("CREATE TABLE t (blob BYTES)");
29498 let Statement::CreateTable(c) = s else {
29499 panic!("expected CreateTable");
29500 };
29501 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29502 }
29503
29504 #[test]
29505 fn bytea_round_trip_display() {
29506 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29507 let again = parse_statement(&original.to_string()).unwrap();
29508 assert_eq!(original, again);
29509 }
29510
29511 // --- v0.9 transactions -------------------------------------------------
29512
29513 #[test]
29514 fn begin_commit_rollback_parse_as_unit_variants() {
29515 let plain = crate::ast::TransactionModes::default();
29516 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29517 assert_eq!(parse("COMMIT"), Statement::Commit);
29518 // r1066 — PG synonyms pgbench's tpcb script relies on.
29519 assert_eq!(parse("END"), Statement::Commit);
29520 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29521 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29522 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29523 // Trailing semicolons accepted too.
29524 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29525 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29526 // statement (with or without the WORK/TRANSACTION noise word).
29527 assert_eq!(
29528 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29529 Statement::Begin(crate::ast::TransactionModes {
29530 isolation: Some(IsolationLevel::RepeatableRead),
29531 read_only: None,
29532 })
29533 );
29534 assert_eq!(
29535 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29536 Statement::Begin(crate::ast::TransactionModes {
29537 isolation: Some(IsolationLevel::Serializable),
29538 read_only: None,
29539 })
29540 );
29541 // v7.39 — this line used to read
29542 //
29543 // // A non-isolation mode keeps the session default (None).
29544 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29545 //
29546 // which pinned the defect rather than catching it: the READ ONLY
29547 // was thrown away, so the statement opened an ordinary read-write
29548 // transaction and every write inside it was accepted. The
29549 // isolation level is still absent here, because this statement
29550 // does not name one — that part was right.
29551 assert_eq!(
29552 parse("BEGIN READ ONLY"),
29553 Statement::Begin(crate::ast::TransactionModes {
29554 isolation: None,
29555 read_only: Some(true),
29556 })
29557 );
29558 assert_eq!(
29559 parse("START TRANSACTION READ WRITE"),
29560 Statement::Begin(crate::ast::TransactionModes {
29561 isolation: None,
29562 read_only: Some(false),
29563 })
29564 );
29565 assert_eq!(
29566 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29567 Statement::Begin(crate::ast::TransactionModes {
29568 isolation: Some(IsolationLevel::Serializable),
29569 read_only: Some(true),
29570 })
29571 );
29572 }
29573
29574 // --- v1.2: pgvector distance ops + ::vector cast --------------------
29575
29576 #[test]
29577 fn inner_product_binop_parses() {
29578 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29579 let Statement::Select(s) = s else { panic!() };
29580 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29581 panic!()
29582 };
29583 assert!(matches!(
29584 expr,
29585 Expr::Binary {
29586 op: BinOp::InnerProduct,
29587 ..
29588 }
29589 ));
29590 }
29591
29592 #[test]
29593 fn cosine_distance_binop_parses() {
29594 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29595 let Statement::Select(s) = s else { panic!() };
29596 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29597 panic!()
29598 };
29599 assert!(matches!(
29600 expr,
29601 Expr::Binary {
29602 op: BinOp::CosineDistance,
29603 ..
29604 }
29605 ));
29606 }
29607
29608 #[test]
29609 fn vector_cast_postfix_wraps_string_literal() {
29610 let s = parse("SELECT '[1,2,3]'::vector FROM t");
29611 let Statement::Select(s) = s else { panic!() };
29612 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29613 panic!()
29614 };
29615 assert!(matches!(
29616 expr,
29617 Expr::Cast {
29618 target: CastTarget::Vector,
29619 ..
29620 }
29621 ));
29622 }
29623
29624 #[test]
29625 fn unsupported_cast_target_errors() {
29626 // v7.37.5 ship triage promoted the parser to accept every
29627 // ident as a `CastTarget::Named(canonical)`; the engine
29628 // surfaces the "unsupported cast target" error at eval
29629 // time when `type_name_to_data_type` can't resolve it.
29630 // Parser-side error now requires a NON-ident after `::`
29631 // (e.g. a punctuation token).
29632 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29633 assert_eq!(err.message, "syntax error at or near \",\"");
29634 }
29635
29636 #[test]
29637 fn tx_statements_round_trip() {
29638 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29639 let original = parse(q);
29640 let again = parse_statement(&original.to_string()).unwrap();
29641 assert_eq!(original, again);
29642 }
29643 }
29644
29645 #[test]
29646 fn interval_text_parsing_units() {
29647 // v7.37.5 β — three-field shape `(months, days, micros)` so
29648 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29649 // Single unit.
29650 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29651 assert_eq!(
29652 parse_interval_text("24 hours"),
29653 Some((0, 0, 86_400_000_000))
29654 );
29655 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29656 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29657 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29658 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29659 // Compound spans accumulate per-dimension.
29660 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29661 assert_eq!(
29662 parse_interval_text("1 day 2 hours"),
29663 Some((0, 1, 7_200_000_000))
29664 );
29665 // Negative numbers carry through per-dimension.
29666 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29667 // Bad shapes return None.
29668 assert_eq!(parse_interval_text(""), None);
29669 assert_eq!(parse_interval_text("garbage"), None);
29670 assert_eq!(parse_interval_text("1 fortnight"), None);
29671 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29672 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29673 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29674 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29675 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29676 }
29677
29678 #[test]
29679 fn interval_literal_roundtrips_via_display() {
29680 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29681 let s = parsed.to_string();
29682 // Display preserves the original text verbatim.
29683 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29684 // And re-parsing yields a structurally equal statement.
29685 let again = parse_statement(&s).unwrap();
29686 assert_eq!(parsed, again);
29687 }
29688
29689 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29690
29691 #[test]
29692 fn parser_recognises_create_publication_bare() {
29693 let s = parse("CREATE PUBLICATION pub_a");
29694 let Statement::CreatePublication(p) = s else {
29695 panic!("expected CreatePublication, got {s:?}")
29696 };
29697 assert_eq!(p.name, "pub_a");
29698 assert_eq!(p.scope, PublicationScope::AllTables);
29699 }
29700
29701 #[test]
29702 fn parser_recognises_create_publication_for_all_tables() {
29703 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29704 let Statement::CreatePublication(p) = s else {
29705 panic!("expected CreatePublication, got {s:?}")
29706 };
29707 assert_eq!(p.name, "pub_a");
29708 assert_eq!(p.scope, PublicationScope::AllTables);
29709 }
29710
29711 #[test]
29712 fn parser_recognises_drop_publication() {
29713 let s = parse("DROP PUBLICATION pub_a");
29714 let Statement::DropPublication { name, .. } = s else {
29715 panic!("expected DropPublication, got {s:?}")
29716 };
29717 assert_eq!(name, "pub_a");
29718 }
29719
29720 #[test]
29721 fn parser_recognises_for_table_list() {
29722 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29723 let Statement::CreatePublication(p) = s else {
29724 panic!("expected CreatePublication, got {s:?}")
29725 };
29726 assert_eq!(p.name, "pub_a");
29727 let PublicationScope::ForTables(ts) = p.scope else {
29728 panic!("expected ForTables scope")
29729 };
29730 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29731 }
29732
29733 #[test]
29734 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29735 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29736 // is rejected (`invalid publication object list`; the old
29737 // test pinned an unverifiable "PG 19 accepts both" claim);
29738 // TABLES pairs with IN SCHEMA.
29739 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29740 .expect_err("bare FOR TABLES must reject");
29741 assert!(
29742 alloc::format!("{err}").contains("invalid publication object list"),
29743 "got: {err}"
29744 );
29745 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29746 let Statement::CreatePublication(p) = s else {
29747 panic!("expected CreatePublication, got {s:?}")
29748 };
29749 let PublicationScope::TablesInSchema(schema) = p.scope else {
29750 panic!("expected TablesInSchema")
29751 };
29752 assert_eq!(schema, "public");
29753 }
29754
29755 #[test]
29756 fn parser_recognises_for_all_tables_except_list() {
29757 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29758 let Statement::CreatePublication(p) = s else {
29759 panic!()
29760 };
29761 let PublicationScope::AllTablesExcept(ts) = p.scope else {
29762 panic!("expected AllTablesExcept")
29763 };
29764 assert_eq!(ts, alloc::vec!["t1", "t2"]);
29765 }
29766
29767 #[test]
29768 fn parser_rejects_for_table_with_empty_list() {
29769 // `FOR TABLE` with nothing after is a parse error.
29770 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29771 .expect_err("must error on empty list");
29772 // No specific message asserted — the call falls through to
29773 // expect_ident_like which yields "expected identifier, got …".
29774 assert!(!err.message.is_empty());
29775 }
29776
29777 #[test]
29778 fn parser_recognises_show_publications() {
29779 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29780 // bare ident in this position, NOT a reserved keyword.
29781 let s = parse("SHOW PUBLICATIONS");
29782 assert!(matches!(s, Statement::ShowPublications));
29783 }
29784
29785 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29786
29787 #[test]
29788 fn parser_recognises_create_subscription_single_publication() {
29789 let s = parse(
29790 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29791 );
29792 let Statement::CreateSubscription(c) = s else {
29793 panic!("expected CreateSubscription, got {s:?}")
29794 };
29795 assert_eq!(c.name, "sub_a");
29796 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29797 assert_eq!(c.publications, alloc::vec!["pub_a"]);
29798 }
29799
29800 #[test]
29801 fn parser_recognises_create_subscription_multi_publication() {
29802 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29803 let Statement::CreateSubscription(c) = s else {
29804 panic!()
29805 };
29806 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29807 }
29808
29809 #[test]
29810 fn parser_rejects_create_subscription_missing_connection() {
29811 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29812 .expect_err("must error on missing CONNECTION");
29813 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29814 }
29815
29816 #[test]
29817 fn parser_rejects_create_subscription_missing_publication() {
29818 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29819 .expect_err("must error on missing PUBLICATION");
29820 assert_eq!(err.message, "syntax error at end of input");
29821 }
29822
29823 #[test]
29824 fn parser_recognises_drop_subscription() {
29825 let s = parse("DROP SUBSCRIPTION sub_a");
29826 let Statement::DropSubscription { name, .. } = s else {
29827 panic!("expected DropSubscription, got {s:?}")
29828 };
29829 assert_eq!(name, "sub_a");
29830 }
29831
29832 #[test]
29833 fn parser_recognises_show_subscriptions() {
29834 let s = parse("SHOW SUBSCRIPTIONS");
29835 assert!(matches!(s, Statement::ShowSubscriptions));
29836 }
29837
29838 #[test]
29839 fn parser_recognises_wait_for_wal_position_no_timeout() {
29840 let s = parse("WAIT FOR WAL POSITION 12345");
29841 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29842 panic!("expected WaitForWalPosition, got {s:?}")
29843 };
29844 assert_eq!(pos, 12345);
29845 assert!(timeout_ms.is_none());
29846 }
29847
29848 #[test]
29849 fn parser_recognises_wait_for_wal_position_with_timeout() {
29850 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29851 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29852 panic!()
29853 };
29854 assert_eq!(pos, 67890);
29855 assert_eq!(timeout_ms, Some(5000));
29856 }
29857
29858 #[test]
29859 fn parser_rejects_wait_with_negative_position() {
29860 // The lexer treats `-` as a token; `expect_u64_literal`
29861 // only sees the Integer that follows, so the negative
29862 // arrives as a unary-minus expression at higher levels.
29863 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29864 // parse error one way or another.
29865 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29866 assert!(!err.message.is_empty());
29867 }
29868
29869 #[test]
29870 fn parser_recognises_bare_analyze() {
29871 let s = parse("ANALYZE");
29872 assert!(matches!(s, Statement::Analyze(None)));
29873 }
29874
29875 #[test]
29876 fn parser_recognises_analyze_with_table() {
29877 let s = parse("ANALYZE users");
29878 let Statement::Analyze(Some(name)) = s else {
29879 panic!("expected Analyze, got {s:?}")
29880 };
29881 assert_eq!(name, "users");
29882 }
29883
29884 #[test]
29885 fn parser_recognises_analyze_with_quoted_table() {
29886 let s = parse("ANALYZE \"Mixed Case\"");
29887 let Statement::Analyze(Some(name)) = s else {
29888 panic!()
29889 };
29890 assert_eq!(name, "Mixed Case");
29891 }
29892
29893 #[test]
29894 fn parser_rejects_analyze_with_garbage_token() {
29895 let err = parse_statement("ANALYZE 42").expect_err("must error");
29896 assert!(!err.message.is_empty());
29897 }
29898
29899 #[test]
29900 fn analyze_display_roundtrips() {
29901 for sql in ["ANALYZE", "ANALYZE users"] {
29902 let s = parse(sql);
29903 let printed = s.to_string();
29904 let again = parse_statement(&printed)
29905 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29906 assert_eq!(s, again);
29907 }
29908 }
29909
29910 #[test]
29911 fn wait_for_display_roundtrips() {
29912 for sql in [
29913 "WAIT FOR WAL POSITION 12345",
29914 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29915 ] {
29916 let s = parse(sql);
29917 let printed = s.to_string();
29918 let again = parse_statement(&printed)
29919 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29920 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29921 }
29922 }
29923
29924 #[test]
29925 fn subscription_ddl_display_roundtrips() {
29926 for sql in [
29927 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29928 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29929 "DROP SUBSCRIPTION sub_a",
29930 "SHOW SUBSCRIPTIONS",
29931 ] {
29932 let s = parse(sql);
29933 let printed = s.to_string();
29934 let again = parse_statement(&printed)
29935 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29936 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29937 }
29938 }
29939
29940 #[test]
29941 fn parser_drop_dispatches_user_vs_publication() {
29942 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29943 // tokenises DROP. Both targets must still parse.
29944 let s = parse("DROP USER 'alice'");
29945 let Statement::DropUser { name, .. } = s else {
29946 panic!("expected DropUser, got {s:?}")
29947 };
29948 assert_eq!(name, "alice");
29949 // And DROP PUBLICATION lands the new variant.
29950 let s = parse("DROP PUBLICATION p1");
29951 assert!(matches!(s, Statement::DropPublication { .. }));
29952 }
29953
29954 #[test]
29955 fn publication_ddl_display_roundtrips() {
29956 // Every CREATE PUBLICATION variant must Display → parse →
29957 // same AST. v6.1.3 covers all three scope shapes.
29958 for sql in [
29959 "CREATE PUBLICATION pub_a",
29960 "CREATE PUBLICATION pub_a FOR ALL TABLES",
29961 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29962 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29963 "DROP PUBLICATION pub_a",
29964 "SHOW PUBLICATIONS",
29965 ] {
29966 let s = parse(sql);
29967 let printed = s.to_string();
29968 let again = parse_statement(&printed)
29969 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29970 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29971 }
29972 }
29973
29974 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29975
29976 #[test]
29977 fn create_function_returns_trigger_plpgsql_minimal() {
29978 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29979 let s = parse(sql);
29980 let Statement::CreateFunction(f) = s else {
29981 panic!("expected CreateFunction");
29982 };
29983 assert_eq!(f.name, "noop");
29984 assert!(!f.or_replace);
29985 assert!(f.args.is_empty());
29986 assert!(matches!(f.returns, FunctionReturn::Trigger));
29987 assert_eq!(f.language, "plpgsql");
29988 let FunctionBody::PlPgSql(block) = f.body else {
29989 panic!("expected PlPgSql body");
29990 };
29991 assert_eq!(block.statements.len(), 1);
29992 assert!(matches!(
29993 block.statements[0],
29994 PlPgSqlStmt::Return(ReturnTarget::New)
29995 ));
29996 }
29997
29998 #[test]
29999 fn create_function_or_replace_with_assignment() {
30000 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
30001 // RETURN NEW.
30002 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
30003BEGIN
30004 NEW.search_vector := to_tsvector('english', NEW.subject);
30005 RETURN NEW;
30006END;
30007$$";
30008 let s = parse(sql);
30009 let Statement::CreateFunction(f) = s else {
30010 panic!("expected CreateFunction");
30011 };
30012 assert!(f.or_replace);
30013 let FunctionBody::PlPgSql(block) = &f.body else {
30014 panic!("expected PlPgSql body");
30015 };
30016 assert_eq!(block.statements.len(), 2);
30017 // First statement: NEW.search_vector := to_tsvector(...)
30018 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
30019 panic!("expected Assign as first stmt");
30020 };
30021 match target {
30022 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
30023 other => panic!("expected NEW.col, got {other:?}"),
30024 }
30025 // Second statement: RETURN NEW
30026 assert!(matches!(
30027 block.statements[1],
30028 PlPgSqlStmt::Return(ReturnTarget::New)
30029 ));
30030 }
30031
30032 #[test]
30033 fn create_trigger_after_insert_or_update() {
30034 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
30035 let s = parse(sql);
30036 let Statement::CreateTrigger(t) = s else {
30037 panic!("expected CreateTrigger");
30038 };
30039 assert_eq!(t.name, "tg");
30040 assert_eq!(t.table, "messages");
30041 assert_eq!(t.timing, TriggerTiming::After);
30042 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
30043 assert_eq!(t.for_each, TriggerForEach::Row);
30044 assert_eq!(t.function, "update_sv");
30045 }
30046
30047 #[test]
30048 fn create_trigger_before_delete_execute_procedure_alias() {
30049 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
30050 let sql =
30051 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
30052 let s = parse(sql);
30053 let Statement::CreateTrigger(t) = s else {
30054 panic!("expected CreateTrigger");
30055 };
30056 assert_eq!(t.timing, TriggerTiming::Before);
30057 assert_eq!(t.events, vec![TriggerEvent::Delete]);
30058 }
30059
30060 #[test]
30061 fn drop_trigger_if_exists_round_trips() {
30062 // No parser support for DROP TRIGGER yet — added in v7.12.5
30063 // alongside the broader DROP …{IF EXISTS} cleanup. The
30064 // AST + Display impls are in place so we round-trip via
30065 // construction:
30066 let s = Statement::DropTrigger {
30067 name: "tg".into(),
30068 table: "messages".into(),
30069 if_exists: true,
30070 };
30071 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
30072 }
30073
30074 #[test]
30075 fn trigger_ddl_display_roundtrips_through_parser() {
30076 // CREATE TRIGGER + its referenced CREATE FUNCTION must
30077 // Display → parse → same AST (modulo PL/pgSQL body
30078 // formatting which is parser-canonicalised).
30079 for sql in [
30080 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
30081 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
30082 ] {
30083 let s = parse(sql);
30084 let printed = s.to_string();
30085 let again = parse_statement(&printed)
30086 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30087 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30088 }
30089 }
30090}