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 self.parse_select_tail_into(&mut head)?;
2260 Ok(Statement::Select(head))
2261 }
2262 // SQL-standard `TABLE name` shorthand for
2263 // `SELECT * FROM name` — pg_dump never emits it, but
2264 // psql users and PG docs use it constantly. Set-op
2265 // chains and the ORDER BY/LIMIT tail compose like any
2266 // SELECT head.
2267 Token::Table
2268 if matches!(
2269 self.tokens.get(self.pos + 1),
2270 Some(Token::Ident(_) | Token::QuotedIdent(_))
2271 ) =>
2272 {
2273 let mut head = self.parse_table_shorthand()?;
2274 self.parse_setop_chain_into(&mut head)?;
2275 self.parse_select_tail_into(&mut head)?;
2276 Ok(Statement::Select(head))
2277 }
2278 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2279 // body is a dollar-quoted plpgsql block (lexer already
2280 // collapsed `$$…$$` into a single Token::String).
2281 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2282 // real PlPgSqlBlock so the engine can EXECUTE it at
2283 // top level instead of silently swallowing. Pre-
2284 // v7.16.2 the parser threw the body away and the
2285 // engine returned CommandOk for the entire DO; that
2286 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2287 // $$` into a SEV-1 silent no-op (the IF + the rename
2288 // were both invisible — mailrs's migrate-042 didn't
2289 // actually run). Now the body parses + executes;
2290 // EmbeddedSql inside the block runs immediately
2291 // against the engine (not deferred — we're at top
2292 // level, not inside a trigger row-write loop).
2293 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2294 self.advance();
2295 let body_text = match self.advance() {
2296 Token::String(s) => s,
2297 other => {
2298 return Err(self.err(alloc::format!(
2299 "expected dollar-quoted body after DO, got {other:?}"
2300 )));
2301 }
2302 };
2303 // Optional `LANGUAGE <name>` trailer (idents only).
2304 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2305 self.advance();
2306 let _ = self.expect_ident_like()?;
2307 }
2308 // Parse the body — same shape CREATE FUNCTION
2309 // uses for trigger function bodies. If the body
2310 // doesn't parse cleanly we surface the error
2311 // (better than silent no-op).
2312 let block = parse_plpgsql_body(&body_text)?;
2313 Ok(Statement::DoBlock(block))
2314 }
2315 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2316 // WITH isn't a reserved token in our lexer — comes through
2317 // as `Token::Ident("with")` (case-insensitive).
2318 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2319 self.advance();
2320 self.parse_with_cte_then_select()
2321 }
2322 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2323 // an identifier — not a reserved keyword.
2324 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2325 self.advance();
2326 let mut analyze = false;
2327 let mut suggest = false;
2328 let mut costs_off = false;
2329 let mut buffers = false;
2330 let mut timing_off = false;
2331 let mut settings = false;
2332 let mut wal = false;
2333 let mut summary_off = false;
2334 let mut format = crate::ast::ExplainFormat::Text;
2335 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2336 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2337 // options are comma-separated. Booleans default to ON
2338 // when the value token is omitted (matches PG).
2339 if matches!(self.peek(), Token::LParen) {
2340 self.advance();
2341 loop {
2342 let opt = match self.peek().clone() {
2343 Token::Ident(s) | Token::QuotedIdent(s) => s,
2344 other => {
2345 return Err(self.err(format!(
2346 "expected option keyword inside EXPLAIN (…), got {other:?}"
2347 )));
2348 }
2349 };
2350 self.advance();
2351 if opt.eq_ignore_ascii_case("suggest") {
2352 suggest = true;
2353 // SUGGEST takes no explicit value today.
2354 } else if opt.eq_ignore_ascii_case("costs") {
2355 // PG syntax: `COSTS [ON | OFF]`. Default
2356 // when value omitted is ON, so plain
2357 // `COSTS` is a no-op. `COSTS OFF` flips.
2358 // `ON` lexes to `Token::On` (reserved
2359 // keyword in JOIN ... ON contexts); accept
2360 // it alongside the bare Ident form so the
2361 // grammar matches PG verbatim.
2362 let value = match self.peek().clone() {
2363 Token::On => {
2364 self.advance();
2365 true
2366 }
2367 Token::Ident(v) | Token::QuotedIdent(v)
2368 if v.eq_ignore_ascii_case("off") =>
2369 {
2370 self.advance();
2371 false
2372 }
2373 Token::Ident(v) | Token::QuotedIdent(v)
2374 if v.eq_ignore_ascii_case("true") =>
2375 {
2376 self.advance();
2377 true
2378 }
2379 _ => true,
2380 };
2381 costs_off = !value;
2382 } else if opt.eq_ignore_ascii_case("analyze")
2383 || opt.eq_ignore_ascii_case("analyse")
2384 {
2385 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2386 // Same default-ON rule as ANALYZE keyword form.
2387 let value = match self.peek().clone() {
2388 Token::On => {
2389 self.advance();
2390 true
2391 }
2392 Token::Ident(v) | Token::QuotedIdent(v)
2393 if v.eq_ignore_ascii_case("off") =>
2394 {
2395 self.advance();
2396 false
2397 }
2398 Token::Ident(v) | Token::QuotedIdent(v)
2399 if v.eq_ignore_ascii_case("true") =>
2400 {
2401 self.advance();
2402 true
2403 }
2404 _ => true,
2405 };
2406 analyze = value;
2407 } else if opt.eq_ignore_ascii_case("buffers") {
2408 // v7.37.22 — `BUFFERS [ON|OFF]`.
2409 let value = match self.peek().clone() {
2410 Token::On => {
2411 self.advance();
2412 true
2413 }
2414 Token::Ident(v) | Token::QuotedIdent(v)
2415 if v.eq_ignore_ascii_case("off") =>
2416 {
2417 self.advance();
2418 false
2419 }
2420 Token::Ident(v) | Token::QuotedIdent(v)
2421 if v.eq_ignore_ascii_case("true") =>
2422 {
2423 self.advance();
2424 true
2425 }
2426 _ => true,
2427 };
2428 buffers = value;
2429 } else if opt.eq_ignore_ascii_case("timing") {
2430 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2431 // the measured wall-clock annotation.
2432 let value = match self.peek().clone() {
2433 Token::On => {
2434 self.advance();
2435 true
2436 }
2437 Token::Ident(v) | Token::QuotedIdent(v)
2438 if v.eq_ignore_ascii_case("off") =>
2439 {
2440 self.advance();
2441 false
2442 }
2443 Token::Ident(v) | Token::QuotedIdent(v)
2444 if v.eq_ignore_ascii_case("true") =>
2445 {
2446 self.advance();
2447 true
2448 }
2449 _ => true,
2450 };
2451 timing_off = !value;
2452 } else if opt.eq_ignore_ascii_case("settings") {
2453 settings = true;
2454 } else if opt.eq_ignore_ascii_case("wal") {
2455 wal = true;
2456 } else if opt.eq_ignore_ascii_case("summary") {
2457 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2458 // gates the trailing Planning/Execution Time
2459 // lines now (was accept-and-no-op).
2460 let value = match self.peek().clone() {
2461 Token::On => {
2462 self.advance();
2463 true
2464 }
2465 Token::Ident(v) | Token::QuotedIdent(v)
2466 if v.eq_ignore_ascii_case("off") =>
2467 {
2468 self.advance();
2469 false
2470 }
2471 Token::Ident(v) | Token::QuotedIdent(v)
2472 if v.eq_ignore_ascii_case("true") =>
2473 {
2474 self.advance();
2475 true
2476 }
2477 _ => true,
2478 };
2479 summary_off = !value;
2480 } else if opt.eq_ignore_ascii_case("verbose")
2481 || opt.eq_ignore_ascii_case("format")
2482 {
2483 // v7.37.22 — accept-but-no-op the remaining
2484 // PG options so EXPLAIN-using clients
2485 // (pgAdmin / DataGrip) don't see syntax
2486 // errors. FORMAT takes a value (text /
2487 // json / yaml / xml); skip the next token
2488 // if it's an ident.
2489 if opt.eq_ignore_ascii_case("format") {
2490 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2491 {
2492 self.advance();
2493 format = match v.to_ascii_lowercase().as_str() {
2494 "text" => crate::ast::ExplainFormat::Text,
2495 "json" => crate::ast::ExplainFormat::Json,
2496 "xml" => crate::ast::ExplainFormat::Xml,
2497 "yaml" => crate::ast::ExplainFormat::Yaml,
2498 other => {
2499 return Err(self.err(format!(
2500 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2501 supports text, json, xml, yaml"
2502 )));
2503 }
2504 };
2505 }
2506 } else {
2507 // VERBOSE / SUMMARY take optional ON/OFF;
2508 // consume if present.
2509 if matches!(self.peek(), Token::On) {
2510 self.advance();
2511 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2512 self.peek().clone()
2513 && (v.eq_ignore_ascii_case("off")
2514 || v.eq_ignore_ascii_case("true"))
2515 {
2516 self.advance();
2517 let _ = v;
2518 }
2519 }
2520 } else {
2521 return Err(self.err(format!(
2522 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2523 )));
2524 }
2525 if matches!(self.peek(), Token::Comma) {
2526 self.advance();
2527 continue;
2528 }
2529 break;
2530 }
2531 if !matches!(self.peek(), Token::RParen) {
2532 return Err(self.err(format!(
2533 "expected ')' after EXPLAIN options, got {:?}",
2534 self.peek()
2535 )));
2536 }
2537 self.advance();
2538 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2539 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2540 {
2541 self.advance();
2542 analyze = true;
2543 }
2544 // v7.39 (round 224) — the body may open with WITH (CTEs);
2545 // route through the same CTE-then-SELECT path the top-level
2546 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2547 // too (PG explains INSERT / UPDATE / DELETE).
2548 let inner = match self.peek().clone() {
2549 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2550 self.advance();
2551 self.parse_with_cte_then_select()?
2552 }
2553 Token::Insert => self.parse_insert_stmt(false)?,
2554 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2555 self.advance();
2556 self.parse_update_after_keyword()?
2557 }
2558 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2559 self.advance();
2560 self.parse_delete_after_keyword()?
2561 }
2562 _ => self.parse_select_stmt()?,
2563 };
2564 if !matches!(
2565 inner,
2566 Statement::Select(_)
2567 | Statement::Insert(_)
2568 | Statement::Update(_)
2569 | Statement::Delete(_)
2570 ) {
2571 return Err(self.err(format!(
2572 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2573 )));
2574 }
2575 Ok(Statement::Explain(crate::ast::ExplainStatement {
2576 analyze,
2577 inner: Box::new(inner),
2578 suggest,
2579 costs_off,
2580 buffers,
2581 timing_off,
2582 settings,
2583 wal,
2584 summary_off,
2585 format,
2586 }))
2587 }
2588 Token::Create => self.parse_create_stmt(),
2589 Token::Insert => self.parse_insert_stmt(false),
2590 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2591 // spelling; route to the same handler. DESC is the
2592 // reserved ORDER BY token, so it gets its own arm.
2593 Token::Ident(s)
2594 if s.eq_ignore_ascii_case("describe")
2595 && matches!(
2596 self.tokens.get(self.pos + 1),
2597 Some(Token::Ident(_) | Token::QuotedIdent(_))
2598 ) =>
2599 {
2600 self.advance();
2601 let table = self.expect_ident_like()?;
2602 Ok(Statement::ShowColumns(table))
2603 }
2604 Token::Desc
2605 if matches!(
2606 self.tokens.get(self.pos + 1),
2607 Some(Token::Ident(_) | Token::QuotedIdent(_))
2608 ) =>
2609 {
2610 self.advance();
2611 let table = self.expect_ident_like()?;
2612 Ok(Statement::ShowColumns(table))
2613 }
2614 // `COPY table [(cols)] TO STDOUT` — the export half of
2615 // pg_dump's COPY pair (the FROM stdin half rides the
2616 // embed import path). Options need a format design and
2617 // error honestly.
2618 Token::Ident(s)
2619 if s.eq_ignore_ascii_case("copy")
2620 && matches!(
2621 self.tokens.get(self.pos + 1),
2622 Some(Token::Ident(_) | Token::QuotedIdent(_))
2623 ) =>
2624 {
2625 self.advance(); // COPY
2626 let table = self.expect_ident_like()?;
2627 let columns = if matches!(self.peek(), Token::LParen) {
2628 self.advance();
2629 let mut cols = alloc::vec![self.expect_ident_like()?];
2630 while matches!(self.peek(), Token::Comma) {
2631 self.advance();
2632 cols.push(self.expect_ident_like()?);
2633 }
2634 if !matches!(self.peek(), Token::RParen) {
2635 return Err(self.err(format!(
2636 "expected ')' after COPY column list, got {:?}",
2637 self.peek()
2638 )));
2639 }
2640 self.advance();
2641 Some(cols)
2642 } else {
2643 None
2644 };
2645 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2646 // endpoint. (FROM STDIN still rides the wire/import path —
2647 // its data arrives out of band.)
2648 if matches!(self.peek(), Token::From)
2649 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2650 {
2651 self.advance(); // FROM
2652 let Token::String(path) = self.advance() else {
2653 unreachable!()
2654 };
2655 let options = self.parse_copy_to_options()?;
2656 return Ok(Statement::CopyFromFile {
2657 table,
2658 columns,
2659 path,
2660 options,
2661 });
2662 }
2663 if !matches!(self.peek(), Token::To) {
2664 return Err(self.err(format!(
2665 "COPY: only TO STDOUT is supported here (FROM stdin \
2666 rides the import path); got {:?}",
2667 self.peek()
2668 )));
2669 }
2670 self.advance();
2671 if matches!(self.peek(), Token::String(_)) {
2672 let Token::String(path) = self.advance() else { unreachable!() };
2673 let options = self.parse_copy_to_options()?;
2674 return Ok(Statement::CopyToFile {
2675 table,
2676 columns,
2677 query: None,
2678 path,
2679 options,
2680 });
2681 }
2682 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2683 return Err(self.err(format!(
2684 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2685 self.peek()
2686 )));
2687 }
2688 self.advance();
2689 let options = self.parse_copy_to_options()?;
2690 Ok(Statement::CopyTo {
2691 table,
2692 columns,
2693 query: None,
2694 options,
2695 })
2696 }
2697 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2698 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2699 // result set is streamed in COPY format (PG's query form).
2700 Token::Ident(s)
2701 if s.eq_ignore_ascii_case("copy")
2702 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2703 {
2704 self.advance(); // COPY
2705 self.advance(); // (
2706 let query = self.parse_select_stmt()?;
2707 if !matches!(self.peek(), Token::RParen) {
2708 return Err(self.err(format!(
2709 "expected ')' after COPY query, got {:?}",
2710 self.peek()
2711 )));
2712 }
2713 self.advance(); // )
2714 if !matches!(self.peek(), Token::To) {
2715 return Err(self.err(format!(
2716 "COPY (query): only TO STDOUT is supported, got {:?}",
2717 self.peek()
2718 )));
2719 }
2720 self.advance();
2721 if matches!(self.peek(), Token::String(_)) {
2722 let Token::String(path) = self.advance() else { unreachable!() };
2723 let options = self.parse_copy_to_options()?;
2724 return Ok(Statement::CopyToFile {
2725 table: String::new(),
2726 columns: None,
2727 query: Some(alloc::boxed::Box::new(query)),
2728 path,
2729 options,
2730 });
2731 }
2732 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2733 return Err(self.err(format!(
2734 "COPY (query): TO supports STDOUT only, got {:?}",
2735 self.peek()
2736 )));
2737 }
2738 self.advance();
2739 let options = self.parse_copy_to_options()?;
2740 Ok(Statement::CopyTo {
2741 table: String::new(),
2742 columns: None,
2743 query: Some(alloc::boxed::Box::new(query)),
2744 options,
2745 })
2746 }
2747 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2748 // Shares the INSERT body; the replace flag lowers it
2749 // onto ON CONFLICT DO UPDATE with an empty assignment
2750 // list (engine: replace the whole row).
2751 Token::Ident(s)
2752 if s.eq_ignore_ascii_case("replace")
2753 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2754 {
2755 self.parse_insert_stmt(true)
2756 }
2757 Token::Begin => {
2758 self.advance();
2759 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2760 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2761 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2762 // is consumed first, then the trailing modes — including the
2763 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2764 // WORK/TRANSACTION). The explicit level, when present, rides the
2765 // statement so `exec_begin` applies it for this transaction.
2766 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2767 {
2768 self.advance();
2769 }
2770 let iso = self.parse_isolation_level_clauses()?;
2771 Ok(Statement::Begin(iso))
2772 }
2773 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2774 // for BEGIN. START is contextual in PG too; pattern-match
2775 // on the ident here. Iso clauses are parse-and-ignored,
2776 // same as BEGIN above.
2777 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2778 self.advance();
2779 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2780 {
2781 return Err(self.err(alloc::format!(
2782 "expected TRANSACTION after START, got {:?}",
2783 self.peek()
2784 )));
2785 }
2786 self.advance();
2787 let iso = self.parse_isolation_level_clauses()?;
2788 Ok(Statement::Begin(iso))
2789 }
2790 Token::Commit => {
2791 self.advance();
2792 // PG: `COMMIT [WORK | TRANSACTION]`.
2793 if let Token::Ident(w) = self.peek()
2794 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2795 {
2796 self.advance();
2797 }
2798 Ok(Statement::Commit)
2799 }
2800 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2801 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2802 // every transaction with `END;` and the drop-in aborted on
2803 // it. Only reachable at statement start (CASE … END lives
2804 // inside expressions), so no ambiguity.
2805 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2806 self.advance();
2807 if let Token::Ident(w) = self.peek()
2808 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2809 {
2810 self.advance();
2811 }
2812 Ok(Statement::Commit)
2813 }
2814 Token::Rollback => {
2815 self.advance();
2816 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2817 // savepoint without ending the transaction. Bare
2818 // `ROLLBACK` drops the whole TX.
2819 if matches!(self.peek(), Token::To) {
2820 self.advance();
2821 if matches!(self.peek(), Token::Savepoint) {
2822 self.advance();
2823 }
2824 let name = self.expect_ident_like()?;
2825 Ok(Statement::RollbackToSavepoint(name))
2826 } else {
2827 Ok(Statement::Rollback)
2828 }
2829 }
2830 Token::Savepoint => {
2831 self.advance();
2832 let name = self.expect_ident_like()?;
2833 Ok(Statement::Savepoint(name))
2834 }
2835 Token::Release => {
2836 self.advance();
2837 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2838 // is optional in standard SQL.
2839 if matches!(self.peek(), Token::Savepoint) {
2840 self.advance();
2841 }
2842 let name = self.expect_ident_like()?;
2843 Ok(Statement::ReleaseSavepoint(name))
2844 }
2845 Token::Show => {
2846 self.advance();
2847 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2848 // v6.1.2 promoted TABLES to a reserved keyword (for
2849 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2850 // arrives as `Token::Tables` rather than a bare ident.
2851 // USERS / COLUMNS remain bare idents.
2852 let target = match self.advance() {
2853 Token::Tables => "tables".to_string(),
2854 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2855 // keyword token; recognise it as the SHOW CREATE
2856 // dispatch keyword too.
2857 Token::Create => "create".to_string(),
2858 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2859 // keyword too; let SHOW INDEX FROM parse.
2860 Token::Index => "index".to_string(),
2861 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2862 // reserved (used in aggregate function calls);
2863 // recognise it here so the parser dispatches
2864 // to ShowParameter("all") — the engine returns
2865 // the curated parameter inventory.
2866 Token::All => "all".to_string(),
2867 // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2868 // spelling for the size of the diagnostics area.
2869 // MySQL-dialect only: PostgreSQL 18.4 answers this
2870 // phrase with `syntax error at or near "("`, and a
2871 // PG session must keep getting exactly that rather
2872 // than a message about an unknown parameter.
2873 // `COUNT` arrives as a bare ident; the `(*)` and the
2874 // trailing keyword are consumed here so the whole
2875 // form reaches the engine as one parameter name.
2876 Token::Ident(ref c)
2877 if self.mysql_dialect
2878 && c.eq_ignore_ascii_case("count")
2879 && matches!(self.peek(), Token::LParen) =>
2880 {
2881 self.advance();
2882 if matches!(self.peek(), Token::Star) {
2883 self.advance();
2884 }
2885 if matches!(self.peek(), Token::RParen) {
2886 self.advance();
2887 }
2888 match self.advance() {
2889 Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2890 return Ok(Statement::ShowParameter(
2891 "count(*) warnings".to_string(),
2892 ));
2893 }
2894 other => {
2895 return Err(self.err(format!(
2896 "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2897 )));
2898 }
2899 }
2900 }
2901 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2902 other => {
2903 return Err(self.err(format!(
2904 "expected SHOW target, got {other:?}"
2905 )));
2906 }
2907 };
2908 match target.as_str() {
2909 "tables" => Ok(Statement::ShowTables),
2910 "users" => Ok(Statement::ShowUsers),
2911 // v7.38 轴 4 — `SHOW transaction_isolation`
2912 // returns the currently-selected isolation level.
2913 "transaction_isolation" => Ok(Statement::ShowParameter(
2914 "transaction_isolation".to_string(),
2915 )),
2916 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2917 // TABLE <t>` returns a 2-column row: (Table,
2918 // Create Table). mysqldump emits this for every
2919 // table at scrape time; without it the dump
2920 // round-trip stalls.
2921 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2922 // FROM <t>` (also spelled `SHOW INDEX` and
2923 // `SHOW KEYS`). admin / mysqldump probes use
2924 // it to list per-table indexes.
2925 "indexes" | "index" | "keys" => {
2926 if !matches!(self.peek(), Token::From) {
2927 return Err(self.err(format!(
2928 "expected FROM after SHOW INDEXES, got {:?}",
2929 self.peek()
2930 )));
2931 }
2932 self.advance();
2933 let table = self.expect_ident_like()?;
2934 Ok(Statement::ShowIndexes(table))
2935 }
2936 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2937 // `SHOW VARIABLES`. Both return a 2-column row
2938 // set listing server-side state; clients probe
2939 // them at connect time.
2940 "status" => Ok(Statement::ShowStatus),
2941 "variables" => {
2942 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2943 if matches!(self.peek(), Token::Like) {
2944 self.advance();
2945 let pat = match self.advance() {
2946 Token::String(p) => p,
2947 other => {
2948 return Err(self.err(format!(
2949 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2950 )));
2951 }
2952 };
2953 return Ok(Statement::ShowVariablesLike(pat));
2954 }
2955 Ok(Statement::ShowVariables)
2956 }
2957 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2958 "processlist" => Ok(Statement::ShowProcesslist),
2959 "create" => {
2960 // SHOW CREATE TABLE / VIEW / DATABASE — only
2961 // TABLE is supported in v7.17.
2962 let kind = match self.advance() {
2963 Token::Ident(s) | Token::QuotedIdent(s) => s,
2964 Token::Table => "table".to_string(),
2965 other => {
2966 return Err(self.err(format!(
2967 "expected TABLE after SHOW CREATE, got {other:?}"
2968 )));
2969 }
2970 };
2971 if !kind.eq_ignore_ascii_case("table") {
2972 return Err(self.err(format!(
2973 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2974 )));
2975 }
2976 let name = self.expect_ident_like()?;
2977 Ok(Statement::ShowCreateTable(name))
2978 }
2979 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2980 // (and `SHOW SCHEMAS` alias). The mysql client uses
2981 // it to populate the database selector at connect
2982 // time; without it `mysql -p` errors before the
2983 // first user query.
2984 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2985 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2986 // keyword on its own; it lands here as a bare
2987 // ident. Returning all publications + their
2988 // scope summary.
2989 "publications" => Ok(Statement::ShowPublications),
2990 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2991 "subscriptions" => Ok(Statement::ShowSubscriptions),
2992 "columns" => {
2993 if !matches!(self.peek(), Token::From) {
2994 return Err(self.err(format!(
2995 "expected FROM after SHOW COLUMNS, got {:?}",
2996 self.peek()
2997 )));
2998 }
2999 self.advance();
3000 let table = self.expect_ident_like()?;
3001 Ok(Statement::ShowColumns(table))
3002 }
3003 // v7.38 轴 4 surface — `SHOW <param>` for any
3004 // remaining session / preset parameter name
3005 // (server_version, search_path, client_encoding,
3006 // …). The engine's ShowParameter handler does the
3007 // dispatch; unrecognised names error there with
3008 // a pointer to pg_settings, not at parse time —
3009 // so a driver that issues `SHOW spam_setting`
3010 // gets a clear runtime error instead of a
3011 // confusing "unknown SHOW target".
3012 other => {
3013 // v7.38 (read01 P3.20) — a custom namespaced GUC
3014 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3015 // consume the dotted tail so it round-trips with
3016 // `SET app.foo` / `current_setting('app.foo')`.
3017 let mut full = other.to_string();
3018 while matches!(self.peek(), Token::Dot) {
3019 self.advance();
3020 let seg = self.expect_ident_like()?;
3021 full.push('.');
3022 full.push_str(&seg.to_ascii_lowercase());
3023 }
3024 Ok(Statement::ShowParameter(full))
3025 }
3026 }
3027 }
3028 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3029 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3030 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3031 // arrived as a bare ident; tokenising it dedicatedly
3032 // keeps the dispatch tree small.
3033 Token::Drop => {
3034 self.advance();
3035 match self.peek() {
3036 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3037 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3038 // around DROP ROLE cleanup. SPG has no role-owner
3039 // model, so consume to boundary as a no-op.
3040 Token::Ident(s) | Token::QuotedIdent(s)
3041 if s.eq_ignore_ascii_case("owned") =>
3042 {
3043 // v7.39 (round 696) — still a no-op (SPG has no
3044 // role-owner model), but the ROLE is carried out so
3045 // the engine can refuse one that does not exist,
3046 // which is what PG18 does.
3047 self.advance();
3048 if self.peek_is_by() {
3049 self.advance();
3050 }
3051 let names = self.take_comma_separated_names();
3052 self.consume_until_statement_boundary();
3053 Ok(Statement::ValidateOnly {
3054 kind: crate::ast::ValidateOnlyKind::RoleName,
3055 names,
3056 })
3057 }
3058 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3059 // It drops only a TEMPORARY table, and name resolution
3060 // already prefers the session's own, so the keyword is
3061 // consumed and the ordinary DROP TABLE path runs.
3062 Token::Ident(s) | Token::QuotedIdent(s)
3063 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3064 {
3065 self.advance();
3066 if !matches!(self.peek(), Token::Table) {
3067 return Err(self.err(alloc::format!(
3068 "expected TABLE after DROP TEMPORARY, got {:?}",
3069 self.peek()
3070 )));
3071 }
3072 self.parse_drop_table_after_keyword()
3073 }
3074 Token::Publication => {
3075 self.advance();
3076 // v7.39 (round 754, F31-B4) — the round-753
3077 // audit probe tripped over the missing
3078 // `IF EXISTS` here (syntax error).
3079 let if_exists = self.consume_if_exists();
3080 let name = self.expect_ident_or_string()?;
3081 Ok(Statement::DropPublication { name, if_exists })
3082 }
3083 Token::Subscription => {
3084 self.advance();
3085 let if_exists = self.consume_if_exists();
3086 let name = self.expect_ident_or_string()?;
3087 Ok(Statement::DropSubscription { name, if_exists })
3088 }
3089 Token::Ident(s) | Token::QuotedIdent(s)
3090 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3091 {
3092 self.advance();
3093 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3094 // login user IS a role in PG, and SPG's store holds
3095 // both. `IF EXISTS` is accepted on either spelling.
3096 let if_exists = self.consume_if_exists();
3097 let name = self.expect_ident_or_string()?;
3098 Ok(Statement::DropUser { name, if_exists })
3099 }
3100 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3101 // CREATE DATABASE has parsed since v7.14 and this did
3102 // not, so `DROP DATABASE IF EXISTS x` — what every
3103 // teardown script and pg_dumpall preamble opens with —
3104 // came back as a syntax error, which IF EXISTS cannot
3105 // soften. The name is carried so the engine can answer
3106 // the way PG does; PG never lets this succeed on a
3107 // single-database server, since the name is either
3108 // unknown ("database … does not exist", or a notice
3109 // under IF EXISTS) or the one you are connected to
3110 // ("cannot drop the currently open database").
3111 Token::Ident(s) | Token::QuotedIdent(s)
3112 if s.eq_ignore_ascii_case("database") =>
3113 {
3114 self.advance();
3115 let if_exists = self.consume_if_exists();
3116 let name = self.expect_ident_or_string()?;
3117 self.consume_until_statement_boundary();
3118 Ok(Statement::DropDatabase { name, if_exists })
3119 }
3120 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3121 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3122 self.advance();
3123 let if_exists = self.consume_if_exists();
3124 let name = self.expect_ident_like()?;
3125 // ON <table>
3126 if !matches!(self.peek(), Token::On) {
3127 return Err(self.err(alloc::format!(
3128 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3129 self.peek()
3130 )));
3131 }
3132 self.advance();
3133 let table = self.expect_ident_like()?;
3134 Ok(Statement::DropTrigger {
3135 name,
3136 table,
3137 if_exists,
3138 })
3139 }
3140 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3141 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3142 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3143 self.advance();
3144 let if_exists = self.consume_if_exists();
3145 let name = self.expect_ident_like()?;
3146 if !matches!(self.peek(), Token::On) {
3147 return Err(self.err(alloc::format!(
3148 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3149 self.peek()
3150 )));
3151 }
3152 self.advance();
3153 let table = self.expect_ident_like()?;
3154 // Optional CASCADE / RESTRICT — accepted, no effect.
3155 self.consume_until_statement_boundary();
3156 Ok(Statement::DropRule {
3157 name,
3158 table,
3159 if_exists,
3160 })
3161 }
3162 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3163 // v7.12.4 ignores any optional arg-list (signature-
3164 // based overload disambiguation lands in v7.12.5+).
3165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3166 self.advance();
3167 let if_exists = self.consume_if_exists();
3168 let name = self.expect_ident_like()?;
3169 // v7.39 (read01 round 62) — the argument list identifies
3170 // WHICH overload to drop, so it is captured, not
3171 // discarded. `DROP FUNCTION f` (no list) is legal when
3172 // the name is unambiguous; the engine enforces that.
3173 let args = if matches!(self.peek(), Token::LParen) {
3174 Some(self.parse_function_signature_types()?)
3175 } else {
3176 None
3177 };
3178 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3179 // trailer, which `DROP TABLE` and `DROP INDEX` have
3180 // accepted since v7.14 and this one refused outright.
3181 // pg_dump writes it, so refusing was a parse error in
3182 // the middle of a restore. SPG drops the function
3183 // either way — it tracks no dependents to cascade to —
3184 // which is the same reading the other two give it.
3185 self.consume_drop_behaviour();
3186 Ok(Statement::DropFunction {
3187 name,
3188 args,
3189 if_exists,
3190 })
3191 }
3192 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3193 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3194 // emit DROP TABLE IF EXISTS at the head of every
3195 // CREATE TABLE block so re-importing a dump
3196 // overwrites prior state. SPG accepts and removes
3197 // matching tables; CASCADE/RESTRICT trailers
3198 // accepted silently.
3199 Token::Table => self.parse_drop_table_after_keyword(),
3200 // v7.14.0 — DROP INDEX [IF EXISTS] name
3201 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3202 // for partial-index renames and pgvector
3203 // migrations. SPG removes the matching index;
3204 // IF EXISTS makes the drop idempotent.
3205 Token::Index => {
3206 self.advance();
3207 let if_exists_at = self.pos;
3208 let if_exists = self.consume_if_exists();
3209 let name = self.expect_ident_like()?;
3210 // v7.39.7 — MySQL's own spelling, which SPG
3211 // refused.
3212 //
3213 // `DROP INDEX i ON t` is how MySQL drops an
3214 // index; its names live inside a table, so the
3215 // statement names the table. Measured against
3216 // MySQL 9.7.2: the form above works, and the
3217 // bare `DROP INDEX i` PostgreSQL uses is a 1064
3218 // there. SPG had it exactly backwards on the
3219 // MySQL wire — the bare form accepted, MySQL's
3220 // own a syntax error — so a migration that drops
3221 // an index failed against the drop-in and not
3222 // against the thing it replaces.
3223 let table = if matches!(self.peek(), Token::On) {
3224 self.advance();
3225 Some(self.expect_ident_like()?)
3226 } else {
3227 None
3228 };
3229 if self.mysql_dialect {
3230 // MySQL has no `IF EXISTS` here either:
3231 // `DROP INDEX IF EXISTS i ON t` is a 1064.
3232 if if_exists {
3233 return Err(self.err_at(
3234 if_exists_at,
3235 "MySQL has no IF EXISTS on DROP INDEX".into(),
3236 ));
3237 }
3238 if table.is_none() {
3239 return Err(self.err("expected ON after the index name".into()));
3240 }
3241 }
3242 if matches!(
3243 self.peek(),
3244 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3245 || s.eq_ignore_ascii_case("restrict")
3246 ) {
3247 self.advance();
3248 }
3249 Ok(Statement::DropIndex {
3250 name,
3251 if_exists,
3252 table,
3253 })
3254 }
3255 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3256 // [CASCADE|RESTRICT]. SPG is single-database;
3257 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3258 // name [, name…] [CASCADE | RESTRICT]. Real
3259 // unregister (was silent no-op pre-v7.17).
3260 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3261 self.advance();
3262 let if_exists = self.consume_if_exists();
3263 let mut names = vec![self.expect_ident_like()?];
3264 while matches!(self.peek(), Token::Comma) {
3265 self.advance();
3266 names.push(self.expect_ident_like()?);
3267 }
3268 if matches!(
3269 self.peek(),
3270 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3271 || s.eq_ignore_ascii_case("restrict")
3272 ) {
3273 self.advance();
3274 }
3275 Ok(Statement::DropSchema { names, if_exists })
3276 }
3277 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3278 // name [, name…] [CASCADE|RESTRICT].
3279 Token::Ident(s) | Token::QuotedIdent(s)
3280 if s.eq_ignore_ascii_case("type") =>
3281 {
3282 self.advance();
3283 let if_exists = self.consume_if_exists();
3284 let mut names = vec![self.expect_ident_like()?];
3285 while matches!(self.peek(), Token::Comma) {
3286 self.advance();
3287 names.push(self.expect_ident_like()?);
3288 }
3289 if matches!(
3290 self.peek(),
3291 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3292 || s.eq_ignore_ascii_case("restrict")
3293 ) {
3294 self.advance();
3295 }
3296 Ok(Statement::DropType { names, if_exists })
3297 }
3298 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3299 // name [, name…] [CASCADE|RESTRICT].
3300 Token::Ident(s) | Token::QuotedIdent(s)
3301 if s.eq_ignore_ascii_case("domain") =>
3302 {
3303 self.advance();
3304 let if_exists = self.consume_if_exists();
3305 let mut names = vec![self.expect_ident_like()?];
3306 while matches!(self.peek(), Token::Comma) {
3307 self.advance();
3308 names.push(self.expect_ident_like()?);
3309 }
3310 if matches!(
3311 self.peek(),
3312 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3313 || s.eq_ignore_ascii_case("restrict")
3314 ) {
3315 self.advance();
3316 }
3317 Ok(Statement::DropDomain { names, if_exists })
3318 }
3319 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3320 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3321 Token::Ident(s) | Token::QuotedIdent(s)
3322 if s.eq_ignore_ascii_case("materialized") =>
3323 {
3324 self.advance();
3325 let nxt = self.peek().clone();
3326 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3327 {
3328 return Err(self.err(alloc::format!(
3329 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3330 )));
3331 }
3332 self.advance();
3333 let if_exists = self.consume_if_exists();
3334 let mut names = vec![self.expect_ident_like()?];
3335 while matches!(self.peek(), Token::Comma) {
3336 self.advance();
3337 names.push(self.expect_ident_like()?);
3338 }
3339 if matches!(
3340 self.peek(),
3341 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3342 || s.eq_ignore_ascii_case("restrict")
3343 ) {
3344 self.advance();
3345 }
3346 Ok(Statement::DropMaterializedView { names, if_exists })
3347 }
3348 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3349 // name [, name…] [CASCADE|RESTRICT].
3350 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3351 self.advance();
3352 let if_exists = self.consume_if_exists();
3353 let mut names = vec![self.expect_ident_like()?];
3354 while matches!(self.peek(), Token::Comma) {
3355 self.advance();
3356 names.push(self.expect_ident_like()?);
3357 }
3358 if matches!(
3359 self.peek(),
3360 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3361 || s.eq_ignore_ascii_case("restrict")
3362 ) {
3363 self.advance();
3364 }
3365 Ok(Statement::DropView { names, if_exists })
3366 }
3367 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3368 // [CASCADE|RESTRICT]. Real removal from catalog
3369 // (was a silent no-op pre-v7.17).
3370 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3371 self.advance();
3372 let if_exists = self.consume_if_exists();
3373 let mut names = vec![self.expect_ident_like()?];
3374 while matches!(self.peek(), Token::Comma) {
3375 self.advance();
3376 names.push(self.expect_ident_like()?);
3377 }
3378 if matches!(
3379 self.peek(),
3380 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3381 || s.eq_ignore_ascii_case("restrict")
3382 ) {
3383 self.advance();
3384 }
3385 Ok(Statement::DropSequence { names, if_exists })
3386 }
3387 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3388 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3389 self.advance();
3390 self.parse_drop_policy_after_keyword()
3391 }
3392 // v7.37.17 (17.6 siblings) — DROP <target> for
3393 // targets SPG doesn't natively track. pg_dump
3394 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3395 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3396 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3397 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3398 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3399 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3400 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3401 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3402 // etc. — accept + Empty-return so pg_dump tails
3403 // load through. Materialized-view drop dispatches
3404 // to the existing DropTable path when the token
3405 // is Materialized-View-shaped (elsewhere in
3406 // this parser).
3407 Token::Ident(s) | Token::QuotedIdent(s)
3408 if s.eq_ignore_ascii_case("text")
3409 // The DROP dispatch matches on PEEK — `text` is
3410 // not yet consumed, so SEARCH/CONFIGURATION sit
3411 // at pos+1/pos+2 (the round-695 trap's mirror).
3412 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3413 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3414 {
3415 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3416 // validates the name; DICTIONARY / PARSER / TEMPLATE
3417 // stay in the noise arm below.
3418 self.advance(); // TEXT
3419 self.advance(); // SEARCH
3420 self.advance(); // CONFIGURATION
3421 let if_exists = self.consume_if_exists();
3422 let names = self.take_comma_separated_names();
3423 self.consume_until_statement_boundary();
3424 if if_exists {
3425 return Ok(Statement::Empty);
3426 }
3427 Ok(Statement::ValidateOnly {
3428 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3429 names,
3430 })
3431 }
3432 Token::Ident(s) | Token::QuotedIdent(s)
3433 if matches!(
3434 s.to_ascii_lowercase().as_str(),
3435 "type"
3436 | "domain"
3437 | "operator"
3438 | "cast"
3439 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3440 // TEMPLATE (CONFIGURATION intercepted above).
3441 | "text"
3442 | "materialized"
3443 | "large"
3444 | "role"
3445 | "access"
3446 | "procedure"
3447 | "routine"
3448 ) =>
3449 {
3450 self.consume_until_statement_boundary();
3451 Ok(Statement::Empty)
3452 }
3453 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3454 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3455 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3456 // foreign-data warning family (round 706) so a
3457 // CREATE→DROP sequence in a dump stays consistent.
3458 Token::Ident(s) | Token::QuotedIdent(s)
3459 if s.eq_ignore_ascii_case("server")
3460 || s.eq_ignore_ascii_case("foreign") =>
3461 {
3462 self.advance();
3463 self.consume_until_statement_boundary();
3464 Ok(Statement::ValidateOnly {
3465 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3466 names: Vec::new(),
3467 })
3468 }
3469 Token::Ident(s) | Token::QuotedIdent(s)
3470 if s.eq_ignore_ascii_case("collation")
3471 || s.eq_ignore_ascii_case("tablespace") =>
3472 {
3473 let kind = if s.eq_ignore_ascii_case("collation") {
3474 crate::ast::ValidateOnlyKind::CollationName
3475 } else {
3476 crate::ast::ValidateOnlyKind::TablespaceName
3477 };
3478 self.advance();
3479 let if_exists = self.consume_if_exists();
3480 let names = self.take_comma_separated_names();
3481 self.consume_until_statement_boundary();
3482 if if_exists {
3483 return Ok(Statement::Empty);
3484 }
3485 Ok(Statement::ValidateOnly { kind, names })
3486 }
3487 Token::Ident(s) | Token::QuotedIdent(s)
3488 if s.eq_ignore_ascii_case("event") =>
3489 {
3490 self.advance();
3491 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3492 {
3493 self.advance();
3494 }
3495 let if_exists = self.consume_if_exists();
3496 let names = self.take_comma_separated_names();
3497 self.consume_until_statement_boundary();
3498 if if_exists {
3499 return Ok(Statement::Empty);
3500 }
3501 Ok(Statement::ValidateOnly {
3502 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3503 names,
3504 })
3505 }
3506 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3507 // leave the noise list; see the ValidateOnly kinds.
3508 Token::Ident(s) | Token::QuotedIdent(s)
3509 if s.eq_ignore_ascii_case("conversion")
3510 || s.eq_ignore_ascii_case("language")
3511 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3512 // FIRST — the first draft looked for it after.
3513 || s.eq_ignore_ascii_case("procedural") =>
3514 {
3515 let kind = if s.eq_ignore_ascii_case("conversion") {
3516 crate::ast::ValidateOnlyKind::ConversionName
3517 } else {
3518 crate::ast::ValidateOnlyKind::LanguageName
3519 };
3520 self.advance();
3521 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3522 {
3523 self.advance();
3524 }
3525 let if_exists = self.consume_if_exists();
3526 let names = self.take_comma_separated_names();
3527 self.consume_until_statement_boundary();
3528 if if_exists {
3529 return Ok(Statement::Empty);
3530 }
3531 Ok(Statement::ValidateOnly { kind, names })
3532 }
3533 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3534 // name(argtypes)[, …]`. Parsed for real so the engine
3535 // can answer as PG does; see Statement::DropAggregate.
3536 Token::Ident(s) | Token::QuotedIdent(s)
3537 if s.eq_ignore_ascii_case("aggregate") =>
3538 {
3539 self.advance();
3540 let if_exists = self.consume_if_exists();
3541 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3542 loop {
3543 let name = self.expect_ident_like()?;
3544 if !matches!(self.peek(), Token::LParen) {
3545 return Err(self.err(alloc::format!(
3546 "expected argument list after DROP AGGREGATE {name}"
3547 )));
3548 }
3549 self.advance();
3550 let mut args: Vec<String> = Vec::new();
3551 let mut star = false;
3552 loop {
3553 match self.peek().clone() {
3554 Token::RParen => {
3555 self.advance();
3556 break;
3557 }
3558 Token::Star => {
3559 self.advance();
3560 star = true;
3561 }
3562 Token::Comma => {
3563 self.advance();
3564 }
3565 _ => {
3566 // A type name may be multi-token
3567 // (`double precision`); glue idents
3568 // until , or ).
3569 let mut t = self.expect_ident_like()?;
3570 while let Token::Ident(nx) = self.peek() {
3571 let nx = nx.clone();
3572 self.advance();
3573 t.push(' ');
3574 t.push_str(&nx);
3575 }
3576 args.push(t);
3577 }
3578 }
3579 }
3580 items.push((name, if star { None } else { Some(args) }));
3581 if matches!(self.peek(), Token::Comma) {
3582 self.advance();
3583 } else {
3584 break;
3585 }
3586 }
3587 self.consume_until_statement_boundary();
3588 Ok(Statement::DropAggregate { if_exists, items })
3589 }
3590 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3591 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3592 // installed; `IF EXISTS` is the spelling that says do
3593 // not, and it keeps the no-op.
3594 Token::Ident(s) | Token::QuotedIdent(s)
3595 if s.eq_ignore_ascii_case("extension") =>
3596 {
3597 self.advance();
3598 let if_exists = self.consume_if_exists();
3599 let names = self.take_comma_separated_names();
3600 self.consume_until_statement_boundary();
3601 if if_exists {
3602 return Ok(Statement::Empty);
3603 }
3604 Ok(Statement::ValidateOnly {
3605 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3606 names,
3607 })
3608 }
3609 Token::Ident(s) | Token::QuotedIdent(s)
3610 if s.eq_ignore_ascii_case("statistics") =>
3611 {
3612 self.parse_drop_statistics_after_drop()
3613 }
3614 other => Err(self.err(format!(
3615 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3616 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3617 ))),
3618 }
3619 }
3620 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3621 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3622 // and accepted before the view name. SPG materialised
3623 // views re-evaluate on read (always-fresh semantics), so
3624 // the CONCURRENTLY-vs-serial distinction has no runtime
3625 // effect — the refresh body does not block readers either
3626 // way. Same accept-and-no-op pattern as DETACH PARTITION
3627 // CONCURRENTLY (16.5).
3628 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3629 self.advance();
3630 let nxt = self.peek().clone();
3631 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3632 {
3633 return Err(self.err(alloc::format!(
3634 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3635 )));
3636 }
3637 self.advance();
3638 let nxt2 = self.peek().clone();
3639 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3640 {
3641 return Err(self.err(alloc::format!(
3642 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3643 )));
3644 }
3645 self.advance();
3646 // Optional CONCURRENTLY noise word — consumed without
3647 // changing semantics.
3648 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3649 {
3650 self.advance();
3651 }
3652 let name = self.expect_ident_like()?;
3653 let with_data = self.parse_optional_with_data(true)?;
3654 Ok(Statement::RefreshMaterializedView { name, with_data })
3655 }
3656 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3657 self.advance();
3658 self.parse_update_after_keyword()
3659 }
3660 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3661 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3662 // [CASCADE | RESTRICT]. Clears every row from each named
3663 // table. Parses at the top level; the engine dispatcher
3664 // walks Statement::Truncate.
3665 // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3666 //
3667 // PostgreSQL renames a table through `ALTER TABLE … RENAME
3668 // TO`, which SPG already had, so this spelling answered 1064
3669 // — and it is what a MySQL migration writes. Measured on
3670 // 9.7.2: several pairs in one statement are accepted, and
3671 // renaming onto a name that exists is 1050.
3672 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3673 self.advance();
3674 if matches!(self.peek(), Token::Table) {
3675 self.advance();
3676 }
3677 let mut pairs: Vec<(String, String)> = Vec::new();
3678 loop {
3679 let from = self.expect_ident_like()?;
3680 if matches!(self.peek(), Token::To) {
3681 self.advance();
3682 } else {
3683 self.expect_keyword_ident("to")?;
3684 }
3685 let to = self.expect_ident_like()?;
3686 pairs.push((from, to));
3687 if matches!(self.peek(), Token::Comma) {
3688 self.advance();
3689 } else {
3690 break;
3691 }
3692 }
3693 Ok(Statement::RenameTables(pairs))
3694 }
3695 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3696 self.advance();
3697 // Optional TABLE noise word — PG accepts both the reserved
3698 // token and the bare identifier spelling.
3699 if matches!(self.peek(), Token::Table)
3700 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3701 {
3702 self.advance();
3703 }
3704 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3705 // not absorbed. The lookahead keeps a table genuinely
3706 // called `only` working: the keyword is a keyword only
3707 // when a name follows it.
3708 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3709 if s.eq_ignore_ascii_case("only"))
3710 && matches!(
3711 self.tokens.get(self.pos + 1),
3712 Some(Token::Ident(_) | Token::QuotedIdent(_))
3713 );
3714 if only {
3715 self.advance();
3716 }
3717 // Table names (comma-separated).
3718 let mut tables = Vec::new();
3719 loop {
3720 tables.push(self.expect_ident_like()?);
3721 if matches!(self.peek(), Token::Comma) {
3722 self.advance();
3723 continue;
3724 }
3725 break;
3726 }
3727 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3728 let mut restart_identity = false;
3729 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3730 {
3731 self.advance();
3732 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3733 {
3734 self.advance();
3735 restart_identity = true;
3736 }
3737 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3738 {
3739 self.advance();
3740 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3741 {
3742 self.advance();
3743 }
3744 }
3745 // Optional CASCADE / RESTRICT.
3746 let mut cascade = false;
3747 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3748 {
3749 self.advance();
3750 cascade = true;
3751 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3752 {
3753 self.advance();
3754 }
3755 Ok(Statement::Truncate {
3756 tables,
3757 restart_identity,
3758 cascade,
3759 only,
3760 })
3761 }
3762 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3763 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3764 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3765 // rows change so the index tree is always up-to-date;
3766 // REINDEX is a strict no-op. Accept the whole statement
3767 // shape to boundary for pg_dump round-trip compatibility.
3768 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3769 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3770 // index bloat to rebuild, so the work stays a no-op, but PG
3771 // validates what it was pointed at and this swallowed the
3772 // name at parse time — `REINDEX TABLE typo` reported
3773 // success. Measured on PG18: INDEX / TABLE name a relation,
3774 // SCHEMA a schema, SYSTEM nothing.
3775 self.advance();
3776 self.parse_reindex_tail()
3777 }
3778 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3779 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3780 // SPG has no MVCC bloat today (Phase D visibility map
3781 // queues with v7.38); the freezer collapses hot-tier
3782 // rows into cold segments automatically. VACUUM is a
3783 // no-op — pg_dump maintenance scripts and Discourse's
3784 // periodic-maintenance path both emit it.
3785 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3786 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3787 // actual bloat, so the pre-MVCC accept-and-ignore posture
3788 // became a silent no-op on a customer's manual reclaim.
3789 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3790 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3791 // ANALYZE is captured, the optional table name is captured.
3792 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3793 self.advance();
3794 // Parenthesised option list: absorb it.
3795 if matches!(self.peek(), Token::LParen) {
3796 let mut depth = 0usize;
3797 loop {
3798 match self.advance() {
3799 Token::LParen => depth += 1,
3800 Token::RParen => {
3801 depth -= 1;
3802 if depth == 0 {
3803 break;
3804 }
3805 }
3806 Token::Eof => break,
3807 _ => {}
3808 }
3809 }
3810 }
3811 let mut analyze = false;
3812 let mut table: Option<String> = None;
3813 loop {
3814 match self.peek() {
3815 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3816 // an identifier, so the loop below broke out on it and
3817 // dropped the table name: `VACUUM FULL nosuch` was
3818 // accepted where `VACUUM nosuch` was refused.
3819 Token::Full => {
3820 self.advance();
3821 }
3822 Token::Ident(w) | Token::QuotedIdent(w) => {
3823 let wl = w.to_ascii_lowercase();
3824 match wl.as_str() {
3825 "full" | "freeze" | "verbose" => {
3826 self.advance();
3827 }
3828 "analyze" | "analyse" => {
3829 analyze = true;
3830 self.advance();
3831 }
3832 _ => {
3833 table = Some(self.expect_ident_like()?);
3834 break;
3835 }
3836 }
3837 }
3838 _ => break,
3839 }
3840 }
3841 // Optional trailing column list / anything else to the
3842 // statement boundary (PG accepts per-column ANALYZE).
3843 self.consume_until_statement_boundary();
3844 Ok(Statement::Vacuum { table, analyze })
3845 }
3846 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3847 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3848 // <index>. PG stores rows in physical order matching
3849 // an index; SPG's hot-tier is append-only + cold-tier
3850 // is segment-frozen, so clustering has no persistent
3851 // effect. Accept-and-no-op for pg_dump compat.
3852 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3853 // v7.39 (round 535) — same as REINDEX above: the relation is
3854 // carried so the engine can refuse one that does not exist.
3855 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3856 self.advance();
3857 self.parse_cluster_tail()
3858 }
3859 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3860 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3861 // optional string payload; UNLISTEN takes a channel or `*`.
3862 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3863 self.advance();
3864 let ch = match self.advance() {
3865 Token::Ident(c) | Token::QuotedIdent(c) => c,
3866 other => {
3867 return Err(self.err(format!(
3868 "expected channel name after LISTEN, got {other:?}"
3869 )));
3870 }
3871 };
3872 Ok(Statement::Listen(ch))
3873 }
3874 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3875 self.advance();
3876 let channel = match self.advance() {
3877 Token::Ident(c) | Token::QuotedIdent(c) => c,
3878 other => {
3879 return Err(self.err(format!(
3880 "expected channel name after NOTIFY, got {other:?}"
3881 )));
3882 }
3883 };
3884 let payload = if matches!(self.peek(), Token::Comma) {
3885 self.advance();
3886 match self.advance() {
3887 Token::String(p) => Some(p),
3888 other => {
3889 return Err(self.err(format!(
3890 "expected string payload after NOTIFY <channel>, got {other:?}"
3891 )));
3892 }
3893 }
3894 } else {
3895 None
3896 };
3897 Ok(Statement::Notify { channel, payload })
3898 }
3899 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3900 self.advance();
3901 match self.advance() {
3902 Token::Star => Ok(Statement::Unlisten(None)),
3903 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3904 other => Err(self.err(format!(
3905 "expected channel name or * after UNLISTEN, got {other:?}"
3906 ))),
3907 }
3908 }
3909 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3910 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3911 // process-wide write lock today; explicit LOCK has no
3912 // effect. Accept-and-no-op for pg_dump / migration
3913 // compat.
3914 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3915 self.advance();
3916 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3917 // engine holds a process-wide write lock), but the TABLE
3918 // NAME is now carried out so the engine can refuse one that
3919 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3920 // READ|WRITE` is a different statement with the same first
3921 // word; it keeps the old no-op, because a MySQL dump's
3922 // bracket names tables it is about to create.
3923 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3924 if k.eq_ignore_ascii_case("tables"));
3925 if mysql_tables {
3926 self.consume_until_statement_boundary();
3927 return Ok(Statement::Empty);
3928 }
3929 if matches!(self.peek(), Token::Table) {
3930 self.advance();
3931 }
3932 let names = self.take_comma_separated_names();
3933 self.consume_until_statement_boundary();
3934 Ok(Statement::ValidateOnly {
3935 kind: crate::ast::ValidateOnlyKind::LockTable,
3936 names,
3937 })
3938 }
3939 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3940 // durability marker + snapshot in PG. SPG has WAL
3941 // checkpointing on a byte / time schedule (v7.37.10
3942 // 60s / 4 MiB defaults). The bare statement parses to
3943 // `Statement::Empty` here (the no_std engine owns no
3944 // WAL / snapshot); v7.37 Epic Du wires the HOST
3945 // (embedded `Database::execute_buffered`, via
3946 // `sql_is_checkpoint`) to force an immediate synchronous
3947 // checkpoint through `Database::checkpoint` — a real
3948 // durability barrier, matching PG.
3949 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3950 self.advance();
3951 self.consume_until_statement_boundary();
3952 Ok(Statement::Empty)
3953 }
3954 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3955 self.advance();
3956 self.parse_delete_after_keyword()
3957 }
3958 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3959 // ALTER is not a reserved keyword in the lexer — handled
3960 // as a bare ident here.
3961 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3962 self.advance();
3963 self.parse_alter_after_keyword()
3964 }
3965 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3966 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3967 // additions needed.
3968 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3969 self.advance();
3970 self.parse_wait_after_keyword()
3971 }
3972 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3973 // Bare ANALYZE → analyse every user table; ANALYZE
3974 // <name> → re-stats one. The argument is an optional
3975 // ident (or quoted ident); anything else is a parse
3976 // error.
3977 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3978 // `WHERE` filter (carved out per V6_7_DESIGN.md
3979 // STABILITY). Lex order: identifier "compact" → "cold"
3980 // → "segments". Anything else after `COMPACT` is a
3981 // parse error.
3982 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3983 self.advance();
3984 let next = self.peek().clone();
3985 let cold = match next {
3986 Token::Ident(s) | Token::QuotedIdent(s) => s,
3987 _ => {
3988 return Err(
3989 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3990 );
3991 }
3992 };
3993 if !cold.eq_ignore_ascii_case("cold") {
3994 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3995 }
3996 self.advance();
3997 let next = self.peek().clone();
3998 let segments = match next {
3999 Token::Ident(s) | Token::QuotedIdent(s) => s,
4000 _ => {
4001 return Err(self.err(format!(
4002 "expected SEGMENTS after COMPACT COLD, got {:?}",
4003 self.peek()
4004 )));
4005 }
4006 };
4007 if !segments.eq_ignore_ascii_case("segments") {
4008 return Err(self.err(format!(
4009 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4010 )));
4011 }
4012 self.advance();
4013 Ok(Statement::CompactColdSegments)
4014 }
4015 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4016 // Parsed as a case-insensitive identifier since MERGE
4017 // isn't a reserved lexer keyword (collides with the
4018 // mysqldump `ALGORITHM = MERGE` view clause if it
4019 // were); the inner parser drives the rest of the
4020 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4021 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4022 self.advance();
4023 self.parse_merge_after_keyword()
4024 }
4025 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4026 self.advance();
4027 // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4028 // keyword is noise to the parse; what differs is the
4029 // ANSWER, which MySQL returns as a result set — see the
4030 // executor.
4031 let mysql_table_kw = matches!(self.peek(), Token::Table);
4032 if mysql_table_kw {
4033 self.advance();
4034 }
4035 let target = match self.peek() {
4036 Token::Eof | Token::Semicolon => None,
4037 Token::Ident(_) | Token::QuotedIdent(_) => {
4038 Some(self.expect_ident_like()?)
4039 }
4040 other => {
4041 return Err(self.err(format!(
4042 "expected table name or end of statement after ANALYZE, got {other:?}"
4043 )));
4044 }
4045 };
4046 // v7.39 (round 776, F31 J7) — the per-column form
4047 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4048 // here while the VACUUM arm already consumed it; SPG
4049 // analyzes whole tables, so the list parses and is
4050 // accepted like the VACUUM path's.
4051 if target.is_some() && matches!(self.peek(), Token::LParen) {
4052 self.advance();
4053 loop {
4054 let _ = self.expect_ident_like()?;
4055 match self.peek() {
4056 Token::Comma => {
4057 self.advance();
4058 }
4059 Token::RParen => {
4060 self.advance();
4061 break;
4062 }
4063 other => {
4064 return Err(self.err(format!(
4065 "expected ',' or ')' in ANALYZE column list, got {other:?}"
4066 )));
4067 }
4068 }
4069 }
4070 }
4071 Ok(Statement::Analyze(target))
4072 }
4073 // v7.12.1 — `SET <name> [TO|=] <value>`. The
4074 // `default_text_search_config` parameter is consumed
4075 // by the FTS function dispatcher; other parameter
4076 // names are recorded but treated as a no-op so PG
4077 // dump output loads.
4078 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4079 self.advance();
4080 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4081 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4082 // …` which the SessionVar path handles). `LOCAL` is the only
4083 // one that changes semantics — it scopes the change to the
4084 // current transaction — so capture it; SESSION / GLOBAL are
4085 // accepted and treated as the default session scope.
4086 let mut set_local = false;
4087 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4088 let q = s.to_ascii_lowercase();
4089 if q == "local" || q == "session" || q == "global" {
4090 set_local = q == "local";
4091 self.advance();
4092 }
4093 }
4094 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4095 // { DEFAULT | <role> }`. pg_dump's ACL section switches
4096 // to the object owner with it. SPG maps it onto the
4097 // session-role machinery (recorded delta RD-10: PG moves
4098 // session_user too; SPG moves the effective role).
4099 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4100 if s.eq_ignore_ascii_case("authorization"))
4101 {
4102 self.advance(); // AUTHORIZATION
4103 let role = match self.peek().clone() {
4104 Token::Default => {
4105 self.advance();
4106 None
4107 }
4108 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4109 self.advance();
4110 Some(s)
4111 }
4112 _ => None,
4113 };
4114 return Ok(Statement::SetRole(role));
4115 }
4116 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4117 // <collation>]` — change the connection client
4118 // charset. SPG stores UTF-8 always and orders
4119 // bytewise; accept as a no-op.
4120 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4121 {
4122 self.advance();
4123 // v7.39 — this used to parse the clause and throw it
4124 // away ("SPG stores UTF-8 always and orders
4125 // bytewise; accept as a no-op"). That sentence
4126 // stopped being true when collations arrived, and
4127 // once `collation_connection` began driving literal
4128 // comparison, dropping the COLLATE clause became a
4129 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4130 // utf8mb4_general_ci` reported back
4131 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4132 //
4133 // The charset name is emitted as `names` and the
4134 // ENGINE expands it, because which collation a
4135 // charset defaults to is MySQL semantics and belongs
4136 // beside the rest of them, not in the parser.
4137 let mut pairs = alloc::vec::Vec::new();
4138 if matches!(
4139 self.peek(),
4140 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4141 ) {
4142 let charset = match self.advance() {
4143 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4144 _ => unreachable!("peeked an ident-or-string"),
4145 };
4146 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4147 }
4148 // Optional `COLLATE <name>` — emitted AFTER `names`
4149 // so it overrides the charset's default, which is
4150 // what MySQL does.
4151 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4152 {
4153 self.advance();
4154 if matches!(
4155 self.peek(),
4156 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4157 ) {
4158 let coll = match self.advance() {
4159 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4160 _ => unreachable!("peeked an ident-or-string"),
4161 };
4162 pairs.push((
4163 String::from("collation_connection"),
4164 crate::ast::SetValue::Ident(coll),
4165 ));
4166 }
4167 }
4168 if pairs.is_empty() {
4169 return Ok(Statement::Empty);
4170 }
4171 return Ok(Statement::SetParameterList(pairs));
4172 }
4173 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4174 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4175 // uses this to switch to the object owner before
4176 // recreating tables. SPG has no role system so this
4177 // is a no-op.
4178 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4179 {
4180 self.advance(); // ROLE
4181 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4182 // reset to the login identity; a name / string sets the
4183 // effective role that drives current_user + RLS.
4184 let role = match self.peek().clone() {
4185 Token::Default => {
4186 self.advance();
4187 None
4188 }
4189 Token::Ident(s) | Token::QuotedIdent(s)
4190 if s.eq_ignore_ascii_case("none") =>
4191 {
4192 self.advance();
4193 None
4194 }
4195 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4196 self.advance();
4197 Some(s)
4198 }
4199 _ => None,
4200 };
4201 return Ok(Statement::SetRole(role));
4202 }
4203 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4204 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4205 // ISO SQL surface). pg_dump prepends this to fix
4206 // the isolation level for the restore session. SPG
4207 // defaults to READ COMMITTED and doesn't yet honor
4208 // session-set isolation across statements — accept
4209 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4210 // per-tx form is handled elsewhere.
4211 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4212 {
4213 self.advance(); // CHARACTERISTICS
4214 if matches!(self.peek(), Token::As) {
4215 self.advance();
4216 }
4217 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4218 self.advance();
4219 }
4220 // v7.39 — no longer a no-op. The note above said SPG
4221 // "doesn't yet honor session-set isolation across
4222 // statements"; it does now, through
4223 // `default_transaction_isolation`, and measured on
4224 // PG 18.6 this statement is exactly a way to set it:
4225 //
4226 // SET SESSION CHARACTERISTICS AS TRANSACTION
4227 // ISOLATION LEVEL REPEATABLE READ;
4228 // current_setting('default_transaction_isolation')
4229 // -> repeatable read
4230 //
4231 // pg_dump prepends this to fix the level for a
4232 // restore session, so accepting it and doing nothing
4233 // meant the restore ran at a level nobody chose.
4234 //
4235 // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4236 // still consumed and dropped. `default_transaction_read_only`
4237 // exists in the GUC inventory but nothing enforces it,
4238 // and setting a value no code honours is the very
4239 // defect this version is about — a session told it
4240 // holds a guarantee it does not.
4241 let modes = self.parse_isolation_level_clauses()?;
4242 self.consume_until_statement_boundary();
4243 let mut pairs: alloc::vec::Vec<(
4244 alloc::string::String,
4245 crate::ast::SetValue,
4246 )> = alloc::vec::Vec::new();
4247 if let Some(level) = modes.isolation {
4248 pairs.push((
4249 alloc::string::String::from("default_transaction_isolation"),
4250 crate::ast::SetValue::String(alloc::string::String::from(
4251 level.as_pg_str(),
4252 )),
4253 ));
4254 }
4255 if let Some(ro) = modes.read_only {
4256 pairs.push((
4257 alloc::string::String::from("default_transaction_read_only"),
4258 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4259 "on"
4260 } else {
4261 "off"
4262 })),
4263 ));
4264 }
4265 return Ok(if pairs.is_empty() {
4266 Statement::Empty
4267 } else {
4268 Statement::SetParameterList(pairs)
4269 });
4270 }
4271 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4272 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4273 // pg_dump emits this to control the deferrability of
4274 // FK / UNIQUE constraints across a bulk restore. SPG
4275 // has no deferrable-constraint machinery today; the
4276 // FK checker is strict-immediate. Accept-and-no-op
4277 // for pg_dump round-trip compatibility.
4278 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4279 {
4280 self.advance(); // CONSTRAINTS
4281 // v7.39 (round 288) — no longer a no-op: the trailing
4282 // DEFERRED / IMMEDIATE sets the transaction's timing.
4283 // v7.39 (round 308, V29) — and the names are kept.
4284 // They used to be skipped over on the way to the
4285 // DEFERRED keyword, so a named form silently behaved
4286 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4287 // every deferrable constraint in the transaction.
4288 let mut names: alloc::vec::Vec<alloc::string::String> =
4289 alloc::vec::Vec::new();
4290 if matches!(self.peek(), Token::All) {
4291 self.advance();
4292 } else {
4293 loop {
4294 let mut n = self.expect_ident_like()?;
4295 // A schema-qualified name (`public.fk_a`)
4296 // identifies the same constraint; PG resolves
4297 // it by the trailing segment.
4298 while matches!(self.peek(), Token::Dot) {
4299 self.advance();
4300 n = self.expect_ident_like()?;
4301 }
4302 names.push(n);
4303 if matches!(self.peek(), Token::Comma) {
4304 self.advance();
4305 } else {
4306 break;
4307 }
4308 }
4309 }
4310 let deferred = match self.peek() {
4311 Token::Ident(s) | Token::QuotedIdent(s)
4312 if s.eq_ignore_ascii_case("deferred") =>
4313 {
4314 true
4315 }
4316 Token::Ident(s) | Token::QuotedIdent(s)
4317 if s.eq_ignore_ascii_case("immediate") =>
4318 {
4319 false
4320 }
4321 other => {
4322 return Err(self.err(alloc::format!(
4323 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4324 )));
4325 }
4326 };
4327 self.advance();
4328 return Ok(Statement::SetConstraints { names, deferred });
4329 }
4330 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4331 // { DEFAULT | '<role>' | <ident> }` (mailrs
4332 // round-10 A.1). pg_dump preamble emits the
4333 // `DEFAULT` form to reset session authorization.
4334 //
4335 // v7.39 (round 697) — this said "SPG has no role system so
4336 // this is a strict no-op". SPG has had one since round 58;
4337 // the comment outlived it, and with it the reason a name
4338 // that is not a role was accepted here. It still switches
4339 // no authorization — what it does now is refuse a role
4340 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4341 // AUTHORIZATION` (handled by the RESET parser
4342 // elsewhere). Reference:
4343 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4344 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4345 {
4346 self.advance(); // AUTHORIZATION
4347 match self.peek().clone() {
4348 Token::Default => {
4349 self.advance();
4350 }
4351 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4352 self.advance();
4353 return Ok(Statement::ValidateOnly {
4354 kind: crate::ast::ValidateOnlyKind::RoleName,
4355 names: alloc::vec![r],
4356 });
4357 }
4358 other => {
4359 return Err(self.err(alloc::format!(
4360 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4361 )));
4362 }
4363 }
4364 return Ok(Statement::Empty);
4365 }
4366 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4367 // ISOLATION LEVEL { READ COMMITTED | READ
4368 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4369 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4370 // PG-standard surface. v7.37.8 accepts the syntax
4371 // and tracks the selected level on
4372 // `Engine::current_isolation_level()`; the actual
4373 // MVCC / SSI semantics implementation lands in
4374 // the 轴 4 isolation framework (separate train).
4375 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4376 // internally; SPG behaves the same (effectively
4377 // READ COMMITTED at every level today).
4378 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4379 {
4380 self.advance(); // TRANSACTION
4381 let modes = self.parse_isolation_level_clauses()?;
4382 return Ok(Statement::SetTransaction { modes });
4383 }
4384 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4385 // alias — same accept-as-no-op as SET NAMES.
4386 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4387 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4388 {
4389 self.advance(); // CHARACTER
4390 self.advance(); // SET
4391 if matches!(
4392 self.peek(),
4393 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4394 ) {
4395 self.advance();
4396 }
4397 return Ok(Statement::Empty);
4398 }
4399 // v7.39 (GUC) — PG spells the timezone GUC as two
4400 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4401 // where <value> is a string/ident or the LOCAL /
4402 // DEFAULT keyword (both mean "back to the default").
4403 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4404 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4405 {
4406 self.advance(); // TIME
4407 self.advance(); // ZONE
4408 let value = match self.peek().clone() {
4409 Token::Ident(s)
4410 if s.eq_ignore_ascii_case("local")
4411 || s.eq_ignore_ascii_case("default") =>
4412 {
4413 self.advance();
4414 crate::ast::SetValue::Default
4415 }
4416 Token::Default => {
4417 self.advance();
4418 crate::ast::SetValue::Default
4419 }
4420 _ => self.parse_set_value()?,
4421 };
4422 return Ok(Statement::SetParameter {
4423 name: "timezone".into(),
4424 value,
4425 local: set_local,
4426 });
4427 }
4428 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4429 // MySQL USER-variable assignment: its own per-session
4430 // namespace, an arbitrary expression on the right, and `:=`
4431 // as a second spelling of `=`. It used to fall into the
4432 // session-PARAMETER list below, whose values are literals and
4433 // whose store nothing reads back under a `@` name — so the
4434 // assignment reported success and vanished.
4435 //
4436 // A `@@`-prefixed LHS is a real engine setting and keeps the
4437 // old path.
4438 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4439 return self.parse_set_user_vars();
4440 }
4441 // v7.14.0 — multi-assignment form
4442 // `SET a = 1, b = 2, …`. Single-assignment is the
4443 // 1-element case. Each LHS may be a regular ident
4444 // or a SessionVar (`@VAR` / `@@VAR`).
4445 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4446 loop {
4447 let lhs = match self.peek().clone() {
4448 Token::SessionVar(s) => {
4449 self.advance();
4450 s
4451 }
4452 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4453 other => {
4454 return Err(self.err(format!(
4455 "expected parameter name after SET, got {other:?}"
4456 )));
4457 }
4458 };
4459 // Accept either `=` or the bare `TO` keyword.
4460 match self.peek() {
4461 Token::Eq => {
4462 self.advance();
4463 }
4464 Token::To => {
4465 self.advance();
4466 }
4467 other => {
4468 return Err(self.err(format!(
4469 "expected `=` or TO after SET {lhs}, got {other:?}"
4470 )));
4471 }
4472 }
4473 let mut value = self.parse_set_value()?;
4474 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4475 // `, name TO` continues a MySQL-style multi-assign,
4476 // anything else is a PG list VALUE
4477 // (`SET search_path = myschema, public`) folded into
4478 // one comma-joined string.
4479 while matches!(self.peek(), Token::Comma) {
4480 let is_assign = matches!(
4481 self.tokens.get(self.pos + 1),
4482 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4483 ) && matches!(
4484 self.tokens.get(self.pos + 2),
4485 Some(Token::Eq | Token::To)
4486 );
4487 if is_assign {
4488 break;
4489 }
4490 self.advance(); // comma
4491 let next = self.parse_set_value()?;
4492 let joined = alloc::format!(
4493 "{}, {}",
4494 set_value_text(&value),
4495 set_value_text(&next)
4496 );
4497 value = crate::ast::SetValue::String(joined);
4498 }
4499 pairs.push((lhs, value));
4500 if matches!(self.peek(), Token::Comma) {
4501 self.advance();
4502 continue;
4503 }
4504 break;
4505 }
4506 if pairs.len() == 1 {
4507 let (name, value) = pairs.into_iter().next().unwrap();
4508 Ok(Statement::SetParameter {
4509 name,
4510 value,
4511 local: set_local,
4512 })
4513 } else {
4514 Ok(Statement::SetParameterList(pairs))
4515 }
4516 }
4517 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4518 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4519 self.advance();
4520 match self.peek().clone() {
4521 Token::All => {
4522 self.advance();
4523 Ok(Statement::ResetParameter(None))
4524 }
4525 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4526 self.advance();
4527 Ok(Statement::ResetParameter(None))
4528 }
4529 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4530 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4531 self.advance();
4532 Ok(Statement::SetRole(None))
4533 }
4534 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4535 // (pg_dump's return from the owner switch).
4536 Token::Ident(s) | Token::QuotedIdent(s)
4537 if s.eq_ignore_ascii_case("session")
4538 && matches!(
4539 self.tokens.get(self.pos + 1),
4540 Some(Token::Ident(a) | Token::QuotedIdent(a))
4541 if a.eq_ignore_ascii_case("authorization")
4542 ) =>
4543 {
4544 self.advance(); // SESSION
4545 self.advance(); // AUTHORIZATION
4546 Ok(Statement::SetRole(None))
4547 }
4548 _ => {
4549 let name = self.parse_set_param_name()?;
4550 Ok(Statement::ResetParameter(Some(name)))
4551 }
4552 }
4553 }
4554 // v7.39 (round 218) — server-side cursors.
4555 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4556 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4557 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4558 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4559 self.advance();
4560 match self.peek().clone() {
4561 Token::All => {
4562 self.advance();
4563 Ok(Statement::CloseCursor { name: None })
4564 }
4565 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4566 self.advance();
4567 Ok(Statement::CloseCursor { name: None })
4568 }
4569 Token::Ident(n) | Token::QuotedIdent(n) => {
4570 self.advance();
4571 Ok(Statement::CloseCursor { name: Some(n) })
4572 }
4573 other => Err(self.err(format!(
4574 "expected cursor name or ALL after CLOSE, got {other:?}"
4575 ))),
4576 }
4577 }
4578 other => Err(self.err(format!(
4579 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4580 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4581 ))),
4582 }
4583 }
4584
4585 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4586 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4587 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4588 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4589 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4590 self.advance(); // DECLARE
4591 let name = match self.advance() {
4592 Token::Ident(n) | Token::QuotedIdent(n) => n,
4593 other => {
4594 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4595 }
4596 };
4597 let mut scroll: Option<bool> = None;
4598 loop {
4599 match self.peek() {
4600 Token::Ident(s)
4601 if s.eq_ignore_ascii_case("binary")
4602 || s.eq_ignore_ascii_case("insensitive")
4603 || s.eq_ignore_ascii_case("asensitive") =>
4604 {
4605 self.advance();
4606 }
4607 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4608 self.advance();
4609 scroll = Some(true);
4610 }
4611 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4612 {
4613 self.advance(); // NO
4614 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4615 return Err(self.err(format!(
4616 "expected SCROLL after NO in DECLARE, got {:?}",
4617 self.peek()
4618 )));
4619 }
4620 self.advance();
4621 scroll = Some(false);
4622 }
4623 _ => break,
4624 }
4625 }
4626 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4627 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4628 }
4629 self.advance();
4630 let mut hold = false;
4631 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4632 self.advance();
4633 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4634 return Err(self.err(format!(
4635 "expected HOLD after WITH in DECLARE, got {:?}",
4636 self.peek()
4637 )));
4638 }
4639 self.advance();
4640 hold = true;
4641 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4642 self.advance();
4643 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4644 return Err(self.err(format!(
4645 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4646 self.peek()
4647 )));
4648 }
4649 self.advance();
4650 }
4651 if !matches!(self.peek(), Token::For) {
4652 return Err(self.err(format!(
4653 "expected FOR before the cursor query, got {:?}",
4654 self.peek()
4655 )));
4656 }
4657 self.advance();
4658 let query = self.parse_one_statement()?;
4659 Ok(Statement::DeclareCursor {
4660 name,
4661 scroll,
4662 hold,
4663 query: alloc::boxed::Box::new(query),
4664 })
4665 }
4666
4667 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4668 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4669 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4670 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4671 use crate::ast::CursorDirection as D;
4672 self.advance(); // FETCH / MOVE
4673 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4674 let neg = if matches!(this.peek(), Token::Minus) {
4675 this.advance();
4676 true
4677 } else {
4678 false
4679 };
4680 match this.advance() {
4681 Token::Integer(v) => Ok(if neg { -v } else { v }),
4682 other => Err(this.err(format!("expected count, got {other:?}"))),
4683 }
4684 };
4685 let direction = match self.peek().clone() {
4686 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4687 self.advance();
4688 D::Next
4689 }
4690 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4691 self.advance();
4692 D::Prior
4693 }
4694 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4695 self.advance();
4696 D::First
4697 }
4698 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4699 self.advance();
4700 D::Last
4701 }
4702 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4703 self.advance();
4704 D::Absolute(signed_count(self)?)
4705 }
4706 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4707 self.advance();
4708 D::Relative(signed_count(self)?)
4709 }
4710 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4711 self.advance();
4712 match self.peek().clone() {
4713 Token::All => {
4714 self.advance();
4715 D::All
4716 }
4717 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4718 self.advance();
4719 D::All
4720 }
4721 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4722 _ => D::Next, // bare FORWARD = FORWARD 1
4723 }
4724 }
4725 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4726 self.advance();
4727 match self.peek().clone() {
4728 Token::All => {
4729 self.advance();
4730 D::BackwardAll
4731 }
4732 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4733 self.advance();
4734 D::BackwardAll
4735 }
4736 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4737 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4738 }
4739 }
4740 Token::All => {
4741 self.advance();
4742 D::All
4743 }
4744 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4745 self.advance();
4746 D::All
4747 }
4748 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4749 // Bare `FETCH <name>` — direction defaults to NEXT.
4750 _ => D::Next,
4751 };
4752 // Optional FROM / IN.
4753 if matches!(self.peek(), Token::From)
4754 || matches!(self.peek(), Token::In)
4755 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4756 {
4757 self.advance();
4758 }
4759 let name = match self.advance() {
4760 Token::Ident(n) | Token::QuotedIdent(n) => n,
4761 other => {
4762 return Err(self.err(format!("expected cursor name, got {other:?}")));
4763 }
4764 };
4765 Ok(if is_move {
4766 Statement::MoveCursor { name, direction }
4767 } else {
4768 Statement::FetchCursor { name, direction }
4769 })
4770 }
4771
4772 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4773 /// [(kind, …)] ON <col>, … FROM <table>`.
4774 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4775 self.advance(); // STATISTICS
4776 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4777 let mut if_not_exists = false;
4778 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4779 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4780 {
4781 self.advance();
4782 self.advance();
4783 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4784 self.advance();
4785 if_not_exists = true;
4786 }
4787 }
4788 let name = self.expect_ident_like()?;
4789 let mut kinds = Vec::new();
4790 if matches!(self.peek(), Token::LParen) {
4791 self.advance();
4792 loop {
4793 let k = self.expect_ident_like()?;
4794 // PG stores the single letters; accept the spelled-out
4795 // names the SQL uses and record what PG records.
4796 kinds.push(match k.to_ascii_lowercase().as_str() {
4797 "ndistinct" => String::from("d"),
4798 "dependencies" => String::from("f"),
4799 "mcv" => String::from("m"),
4800 other => {
4801 return Err(
4802 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4803 );
4804 }
4805 });
4806 match self.advance() {
4807 Token::Comma => {}
4808 Token::RParen => break,
4809 other => {
4810 return Err(self.err(alloc::format!(
4811 "expected ',' or ')' in statistics kind list, got {other:?}"
4812 )));
4813 }
4814 }
4815 }
4816 }
4817 if !matches!(self.peek(), Token::On) {
4818 return Err(self.err(alloc::format!(
4819 "expected ON in CREATE STATISTICS, got {:?}",
4820 self.peek()
4821 )));
4822 }
4823 self.advance();
4824 let mut columns = Vec::new();
4825 loop {
4826 columns.push(self.expect_ident_like()?);
4827 if matches!(self.peek(), Token::Comma) {
4828 self.advance();
4829 } else {
4830 break;
4831 }
4832 }
4833 if !matches!(self.peek(), Token::From) {
4834 return Err(self.err(alloc::format!(
4835 "expected FROM in CREATE STATISTICS, got {:?}",
4836 self.peek()
4837 )));
4838 }
4839 self.advance();
4840 let table = self.expect_ident_like()?;
4841 Ok(Statement::CreateStatistics {
4842 name,
4843 if_not_exists,
4844 kinds,
4845 columns,
4846 table,
4847 })
4848 }
4849
4850 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4851 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4852 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4853 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4854 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4855 /// forward call.
4856 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4857 self.advance(); // TABLE
4858 let if_exists = self.consume_if_exists();
4859 let mut names: Vec<String> = Vec::new();
4860 loop {
4861 names.push(self.expect_ident_like()?);
4862 if matches!(self.peek(), Token::Comma) {
4863 self.advance();
4864 continue;
4865 }
4866 break;
4867 }
4868 if matches!(
4869 self.peek(),
4870 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4871 || s.eq_ignore_ascii_case("restrict")
4872 ) {
4873 self.advance();
4874 }
4875 Ok(Statement::DropTable { names, if_exists })
4876 }
4877
4878 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4879 self.advance(); // STATISTICS
4880 let mut if_exists = false;
4881 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4882 && matches!(self.tokens.get(self.pos + 1),
4883 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4884 {
4885 self.advance();
4886 self.advance();
4887 if_exists = true;
4888 }
4889 let name = self.expect_ident_like()?;
4890 Ok(Statement::DropStatistics { name, if_exists })
4891 }
4892
4893 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4894 debug_assert!(matches!(self.peek(), Token::Create));
4895 self.advance();
4896 match self.peek() {
4897 Token::Table => self.parse_create_table_stmt_after_create(),
4898 Token::Index => self.parse_create_index_stmt_after_create(false),
4899 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4900 // object now. It used to be consumed by the CREATE-noise
4901 // arm, so a pg_dump that declares extended statistics
4902 // restored silently without them and reflection showed
4903 // nothing.
4904 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4905 self.parse_create_statistics_after_create()
4906 }
4907 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4908 // The `UNIQUE` modifier turns a partial index into a
4909 // partial-uniqueness invariant (only rows matching the
4910 // WHERE predicate are checked for duplicates). mailrs
4911 // K1 (3 hits: email_templates default, calendar_events
4912 // master, calendar_events instance).
4913 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4914 self.advance();
4915 if !matches!(self.peek(), Token::Index) {
4916 return Err(self.err(alloc::format!(
4917 "expected INDEX after CREATE UNIQUE, got {:?}",
4918 self.peek()
4919 )));
4920 }
4921 self.parse_create_index_stmt_after_create(true)
4922 }
4923 Token::Publication => {
4924 self.advance();
4925 self.parse_create_publication_after_keyword()
4926 }
4927 Token::Subscription => {
4928 self.advance();
4929 self.parse_create_subscription_after_keyword()
4930 }
4931 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4932 // USER isn't a reserved keyword — we look for the bare
4933 // identifier so the lexer doesn't have to grow a token.
4934 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4935 self.advance();
4936 self.parse_create_user_after_keyword(true)
4937 }
4938 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4939 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4940 // the default of the LOGIN attribute.
4941 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4942 self.advance();
4943 self.parse_create_user_after_keyword(false)
4944 }
4945 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4946 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4947 self.advance();
4948 self.parse_create_policy_after_keyword()
4949 }
4950 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4951 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4952 // no-op. mailrs follow-up F3.
4953 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4954 self.advance();
4955 self.parse_create_extension_after_keyword()
4956 }
4957 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4958 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4959 // optional; absorb it here and forward to the
4960 // per-kind parsers with the flag. OR is a reserved
4961 // keyword token.
4962 Token::Or => {
4963 self.advance();
4964 let next = self.peek();
4965 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4966 return Err(self.err(alloc::format!(
4967 "expected REPLACE after CREATE OR, got {next:?}"
4968 )));
4969 };
4970 if !s2.eq_ignore_ascii_case("replace") {
4971 return Err(self.err(alloc::format!(
4972 "expected REPLACE after CREATE OR, got {s2:?}"
4973 )));
4974 }
4975 self.advance();
4976 self.parse_create_function_or_trigger_after_or_replace(true)
4977 }
4978 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4979 self.advance();
4980 self.parse_create_function_after_keyword(false)
4981 }
4982 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4983 self.advance();
4984 self.parse_create_trigger_after_keyword(false)
4985 }
4986 // v7.39 (round 139) — CREATE RULE …
4987 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4988 self.advance();
4989 self.parse_create_rule_after_keyword(false)
4990 }
4991 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4992 // trigger is a row-level AFTER trigger that additionally carries
4993 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4994 // path already tolerates and skips those clauses, so consuming the
4995 // CONSTRAINT keyword and reusing it makes the statement parse and the
4996 // trigger fire. (The deferral timing itself is not yet honoured —
4997 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4998 // for every non-deferred use.)
4999 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5000 self.advance();
5001 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5002 if t.eq_ignore_ascii_case("trigger"))
5003 {
5004 return Err(self.err(alloc::format!(
5005 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5006 self.peek()
5007 )));
5008 }
5009 self.advance();
5010 self.parse_create_trigger_after_keyword(false)
5011 }
5012 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5013 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5014 self.advance();
5015 self.parse_create_sequence_after_keyword(false)
5016 }
5017 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5018 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5019 self.advance();
5020 self.parse_create_view_after_keyword(false, false, false)
5021 }
5022 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5023 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5024 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5025 // appear (in any order) between `CREATE` and `VIEW` in
5026 // every mysqldump-emitted view. Pre-2.6 the parser
5027 // rejected the prefix and the customer's whole view
5028 // backup failed on the first view. The hints are pure
5029 // planner / permission metadata; SPG's view-rewrite
5030 // path is semantically equivalent for all three
5031 // algorithms in v7.17 (TEMPTABLE differs only in
5032 // perf for huge views — out of v7.17 scope), and
5033 // DEFINER / SQL SECURITY are pure single-user
5034 // permissioning that SPG ignores by design.
5035 Token::Ident(s) | Token::QuotedIdent(s)
5036 if s.eq_ignore_ascii_case("algorithm")
5037 || s.eq_ignore_ascii_case("definer")
5038 || s.eq_ignore_ascii_case("sql") =>
5039 {
5040 self.consume_mysql_view_prefix()?;
5041 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5042 // (in any order, in any combination), the next
5043 // keyword must be VIEW. mysqldump never emits these
5044 // prefixes on non-view statements.
5045 let next = self.peek().clone();
5046 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5047 if s2.eq_ignore_ascii_case("view"))
5048 {
5049 self.advance();
5050 self.parse_create_view_after_keyword(false, false, false)
5051 } else {
5052 Err(self.err(alloc::format!(
5053 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5054 )))
5055 }
5056 }
5057 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5058 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5059 self.advance();
5060 self.parse_create_type_after_keyword()
5061 }
5062 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5063 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5064 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5065 self.advance();
5066 self.parse_create_domain_after_keyword()
5067 }
5068 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5069 // name [AUTHORIZATION user]. Real catalog registry
5070 // (was silent-no-op'd pre-v7.17).
5071 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5072 self.advance();
5073 let if_not_exists = self.parse_if_not_exists();
5074 let name = self.expect_ident_like()?;
5075 // Optional `AUTHORIZATION <user>` trailer — accepted,
5076 // ignored (single-user catalog).
5077 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5078 if s.eq_ignore_ascii_case("authorization"))
5079 {
5080 self.advance();
5081 let _ = self.expect_ident_like()?;
5082 }
5083 Ok(Statement::CreateSchema { name, if_not_exists })
5084 }
5085 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5086 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5087 self.advance();
5088 let next = self.peek().clone();
5089 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5090 {
5091 self.advance();
5092 self.parse_create_materialized_view_after_keyword()
5093 } else {
5094 Err(self.err(alloc::format!(
5095 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5096 )))
5097 }
5098 }
5099 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5100 // no-op below), an UNLOGGED table is a real, fully-usable table in
5101 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5102 // durability optimisation is a follow-up), so a dump / app that
5103 // declares UNLOGGED tables works instead of failing to parse.
5104 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5105 self.advance(); // UNLOGGED
5106 if matches!(self.peek(), Token::Table) {
5107 self.parse_create_table_stmt_after_create()
5108 } else {
5109 Err(self.err(format!(
5110 "expected TABLE after CREATE UNLOGGED, got {:?}",
5111 self.peek()
5112 )))
5113 }
5114 }
5115 Token::Ident(s) | Token::QuotedIdent(s)
5116 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5117 {
5118 self.advance();
5119 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5120 let next = self.peek().clone();
5121 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5122 {
5123 self.advance();
5124 self.parse_create_sequence_after_keyword(true)
5125 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5126 {
5127 self.advance();
5128 self.parse_create_view_after_keyword(false, false, true)
5129 } else {
5130 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5131 // consumed and answered OK while creating nothing, so
5132 // every statement that touched the table afterwards failed
5133 // with "table not found" — the DDL itself lied. It is a
5134 // real CREATE TABLE now, marked temporary so the executor
5135 // puts it in the session's own namespace. An optional
5136 // TABLE keyword may or may not be present (`CREATE TEMP t`
5137 // is not legal, but the keyword is consumed by the
5138 // CREATE TABLE parser itself).
5139 let stmt = self.parse_create_table_stmt_after_create()?;
5140 match stmt {
5141 Statement::CreateTable(mut c) => {
5142 c.temporary = true;
5143 Ok(Statement::CreateTable(c))
5144 }
5145 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5146 // CTAS node, which needs the same session namespace.
5147 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5148 m.temporary = true;
5149 Ok(Statement::CreateMaterializedView(m))
5150 }
5151 other => Ok(other),
5152 }
5153 }
5154 }
5155 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5156 // BEGIN <body> END`. The body may reference `@var`
5157 // session variables, SET statements, internal `;`
5158 // terminators, etc. SPG has no procedure runtime, so
5159 // consume the whole `CREATE PROCEDURE … END` block as
5160 // a no-op so mysqldump scripts that include stored
5161 // routines load through. The matching-END consumer
5162 // tracks BEGIN/END nesting depth to handle nested
5163 // BEGIN blocks correctly.
5164 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5165 self.consume_mysql_routine_body();
5166 Ok(Statement::Empty)
5167 }
5168 // v7.14.0 — pg_dump / mysqldump emit
5169 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5170 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5171 // SPG is single-schema / single-database; these have
5172 // no behavioural effect, so consume + return Empty.
5173 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5174 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5175 // moved up to real parser branches. DATABASE / ROLE /
5176 // POLICY / OPERATOR stay no-op forever
5177 // (single-database, hardcoded roles).
5178 Token::Ident(s) | Token::QuotedIdent(s)
5179 if matches!(
5180 s.to_ascii_lowercase().as_str(),
5181 "database"
5182 | "role"
5183 | "operator"
5184 | "cast"
5185 | "aggregate"
5186 | "language"
5187 | "collation"
5188 | "conversion"
5189 // v7.17.0 Phase 8 (audit N6) — rarely-
5190 // emitted pg_dump shapes that should
5191 // load through without a parser error.
5192 // SPG has no planner statistics catalog,
5193 // no event-trigger hooks, no foreign-
5194 // data-wrapper infrastructure; consume
5195 // + return Empty.
5196 | "statistics"
5197 | "event"
5198 // v7.37.17 (17.6 siblings) — additional CREATE
5199 // targets pg_dump / operator install scripts
5200 // may emit that SPG has no matching machinery
5201 // for. Consume + Empty-return.
5202 | "text"
5203 | "tablespace"
5204 | "access"
5205 | "large"
5206 ) =>
5207 {
5208 // DATABASE is the one member of this list PG refuses
5209 // inside a transaction block; the rest (ROLE, CAST,
5210 // TABLESPACE, …) it runs there quite happily, so only
5211 // this one is named. Still a no-op otherwise — SPG is
5212 // single-database.
5213 let is_database = s.eq_ignore_ascii_case("database");
5214 // The name is the first token after DATABASE, past an
5215 // `IF NOT EXISTS`.
5216 let name = if is_database {
5217 self.scan_database_name()
5218 } else {
5219 None
5220 };
5221 let collation = if is_database {
5222 self.scan_database_collation_until_boundary()
5223 } else {
5224 self.consume_until_statement_boundary();
5225 None
5226 };
5227 if is_database {
5228 return Ok(Statement::NoOpPreventedInTransaction {
5229 what: String::from("CREATE DATABASE"),
5230 collation,
5231 name,
5232 });
5233 }
5234 Ok(Statement::Empty)
5235 }
5236 // v7.39 (round 706) — the foreign-data family leaves the silent
5237 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5238 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5239 // FDW machinery), but the ENGINE now warns, so a restore log
5240 // says what will not function instead of reporting success.
5241 Token::Ident(s) | Token::QuotedIdent(s)
5242 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5243 {
5244 self.consume_until_statement_boundary();
5245 Ok(Statement::ValidateOnly {
5246 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5247 names: Vec::new(),
5248 })
5249 }
5250 other => Err(self.err(format!(
5251 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5252 ))),
5253 }
5254 }
5255
5256 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5257 /// keyword decides whether we parse a function or trigger
5258 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5259 /// PROCEDURE) — those land in later releases.
5260 fn parse_create_function_or_trigger_after_or_replace(
5261 &mut self,
5262 or_replace: bool,
5263 ) -> Result<Statement, ParseError> {
5264 let tok = self.peek();
5265 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5266 return Err(self.err(alloc::format!(
5267 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5268 )));
5269 };
5270 if s.eq_ignore_ascii_case("function") {
5271 self.advance();
5272 self.parse_create_function_after_keyword(or_replace)
5273 } else if s.eq_ignore_ascii_case("trigger") {
5274 self.advance();
5275 self.parse_create_trigger_after_keyword(or_replace)
5276 } else if s.eq_ignore_ascii_case("rule") {
5277 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5278 self.advance();
5279 self.parse_create_rule_after_keyword(or_replace)
5280 } else if s.eq_ignore_ascii_case("view") {
5281 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5282 self.advance();
5283 self.parse_create_view_after_keyword(or_replace, false, false)
5284 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5285 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5286 self.advance();
5287 let nxt = self.peek().clone();
5288 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5289 {
5290 self.advance();
5291 self.parse_create_view_after_keyword(or_replace, false, true)
5292 } else {
5293 Err(self.err(alloc::format!(
5294 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5295 )))
5296 }
5297 } else {
5298 Err(self.err(alloc::format!(
5299 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5300 )))
5301 }
5302 }
5303
5304 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5305 /// SPG doesn't have a registry; pgvector / similar are
5306 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5307 /// the syntax lets dual-target schemas keep the line.
5308 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5309 // Optional `IF NOT EXISTS`.
5310 self.consume_if_not_exists();
5311 let name = self.expect_ident_like()?;
5312 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5313 // CASCADE / FROM '<v>' clauses; we don't model them.
5314 loop {
5315 match self.peek() {
5316 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5317 self.advance();
5318 continue;
5319 }
5320 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5321 self.advance();
5322 let _ = self.expect_ident_like()?;
5323 continue;
5324 }
5325 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5326 self.advance();
5327 // String or ident literal.
5328 let _ = self.advance();
5329 continue;
5330 }
5331 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5332 self.advance();
5333 let _ = self.advance();
5334 continue;
5335 }
5336 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5337 self.advance();
5338 continue;
5339 }
5340 _ => break,
5341 }
5342 }
5343 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5344 // nosuch` reported success and `pg_extension` then did not list it,
5345 // which is the accept-and-do-nothing shape F31 exists to find.
5346 Ok(Statement::ValidateOnly {
5347 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5348 names: alloc::vec![name],
5349 })
5350 }
5351
5352 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5353 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5354 /// already been consumed by the caller. Grammar accepted:
5355 ///
5356 /// name `(` arg-list `)`
5357 /// `RETURNS` return-type
5358 /// [ `LANGUAGE` ident ]
5359 /// `AS` $$ body $$
5360 /// [ `LANGUAGE` ident ]
5361 ///
5362 /// Either `LANGUAGE` position is allowed; PG accepts both.
5363 fn parse_create_function_after_keyword(
5364 &mut self,
5365 or_replace: bool,
5366 ) -> Result<Statement, ParseError> {
5367 let name = self.expect_ident_like()?;
5368 // Argument list. v7.12.4 commonly sees the empty `()`
5369 // (trigger functions); typed args parse and round-trip
5370 // but the executor only invokes nullary functions.
5371 if !matches!(self.peek(), Token::LParen) {
5372 return Err(self.err(alloc::format!(
5373 "expected '(' after function name {name:?}, got {:?}",
5374 self.peek()
5375 )));
5376 }
5377 self.advance();
5378 let args = self.parse_function_arg_list()?;
5379 // RETURNS clause.
5380 let tok = self.peek();
5381 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5382 return Err(self.err(alloc::format!(
5383 "expected RETURNS after function arg list, got {tok:?}"
5384 )));
5385 };
5386 if !s.eq_ignore_ascii_case("returns") {
5387 return Err(self.err(alloc::format!(
5388 "expected RETURNS after function arg list, got {s:?}"
5389 )));
5390 }
5391 self.advance();
5392 let returns = self.parse_function_return()?;
5393 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5394 // re-check after the body too).
5395 let mut language: Option<String> = self.parse_optional_language()?;
5396 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5397 // either side of the body and in any order, interleaved with
5398 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5399 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5400 // PG's own pg_dump output did not restore.
5401 let mut attrs = FunctionAttrs::default();
5402 loop {
5403 let before = self.pos;
5404 self.parse_function_attrs_into(&mut attrs)?;
5405 if language.is_none() {
5406 language = self.parse_optional_language()?;
5407 }
5408 if self.pos == before {
5409 break;
5410 }
5411 }
5412 // `AS` followed by a $$-quoted body (lexer already
5413 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5414 // Token::String). AS is a reserved keyword (Token::As).
5415 if !matches!(self.peek(), Token::As) {
5416 return Err(self.err(alloc::format!(
5417 "expected AS before function body, got {:?}",
5418 self.peek()
5419 )));
5420 }
5421 self.advance();
5422 let body_text = match self.peek() {
5423 Token::String(s) => {
5424 let body = s.clone();
5425 self.advance();
5426 body
5427 }
5428 other => {
5429 return Err(self.err(alloc::format!(
5430 "expected $$-quoted function body after AS, got {other:?}"
5431 )));
5432 }
5433 };
5434 // Trailing clauses — PG's other accepted position for both the
5435 // LANGUAGE and the attributes.
5436 loop {
5437 let before = self.pos;
5438 self.parse_function_attrs_into(&mut attrs)?;
5439 if language.is_none() {
5440 language = self.parse_optional_language()?;
5441 }
5442 if self.pos == before {
5443 break;
5444 }
5445 }
5446 let language = language.unwrap_or_else(|| String::from("sql"));
5447 // PL/pgSQL bodies get structure-parsed. Other languages
5448 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5449 // recognise) round-trip as Raw text — the executor errors
5450 // when invoked with a clear unsupported message.
5451 let body = if language.eq_ignore_ascii_case("plpgsql") {
5452 match parse_plpgsql_body(&body_text) {
5453 Ok(block) => FunctionBody::PlPgSql(block),
5454 // Best-effort: if the body parser doesn't yet
5455 // support a construct used inside, fall back to
5456 // raw — keeps `CREATE FUNCTION` itself working
5457 // (catalogue accepts), executor errors on
5458 // invocation only.
5459 Err(_) => FunctionBody::Raw(body_text),
5460 }
5461 } else {
5462 FunctionBody::Raw(body_text)
5463 };
5464 Ok(Statement::CreateFunction(CreateFunctionStatement {
5465 name,
5466 or_replace,
5467 args,
5468 returns,
5469 language,
5470 body,
5471 attrs,
5472 }))
5473 }
5474
5475 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5476 /// attribute clauses into `attrs`, stopping at the first token that
5477 /// is not one. Measured against PG 18.4, which accepts them in any
5478 /// order and on either side of the body.
5479 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5480 loop {
5481 let word = match self.peek() {
5482 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5483 // NOT LEAKPROOF — NOT is a reserved keyword token.
5484 Token::Not
5485 if matches!(
5486 self.tokens.get(self.pos + 1),
5487 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5488 ) =>
5489 {
5490 self.advance();
5491 self.advance();
5492 attrs.leakproof = false;
5493 continue;
5494 }
5495 _ => return Ok(()),
5496 };
5497 match word.as_str() {
5498 "immutable" => {
5499 self.advance();
5500 attrs.volatility = FunctionVolatility::Immutable;
5501 }
5502 "stable" => {
5503 self.advance();
5504 attrs.volatility = FunctionVolatility::Stable;
5505 }
5506 "volatile" => {
5507 self.advance();
5508 attrs.volatility = FunctionVolatility::Volatile;
5509 }
5510 "strict" => {
5511 self.advance();
5512 attrs.strict = true;
5513 }
5514 "leakproof" => {
5515 self.advance();
5516 attrs.leakproof = true;
5517 }
5518 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5519 // spelled-out forms of STRICT and its opposite.
5520 "returns" | "called" => {
5521 let strict = word == "returns";
5522 let mut probe = self.pos + 1;
5523 if strict {
5524 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5525 // is not ours.
5526 match self.tokens.get(probe) {
5527 Some(Token::Null) => probe += 1,
5528 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5529 _ => return Ok(()),
5530 }
5531 }
5532 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5533 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5534 if !ok {
5535 return Ok(());
5536 }
5537 probe += 1;
5538 match self.tokens.get(probe) {
5539 Some(Token::Null) => probe += 1,
5540 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5541 _ => return Ok(()),
5542 }
5543 match self.tokens.get(probe) {
5544 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5545 _ => return Ok(()),
5546 }
5547 self.pos = probe;
5548 attrs.strict = strict;
5549 }
5550 "security" | "external" => {
5551 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5552 let mut probe = self.pos + 1;
5553 if word == "external" {
5554 match self.tokens.get(probe) {
5555 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5556 probe += 1;
5557 }
5558 _ => return Ok(()),
5559 }
5560 }
5561 let definer = match self.tokens.get(probe) {
5562 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5563 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5564 _ => return Ok(()),
5565 };
5566 self.pos = probe + 1;
5567 attrs.security_definer = definer;
5568 }
5569 "parallel" => {
5570 let level = match self.tokens.get(self.pos + 1) {
5571 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5572 FunctionParallel::Safe
5573 }
5574 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5575 FunctionParallel::Restricted
5576 }
5577 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5578 FunctionParallel::Unsafe
5579 }
5580 _ => return Ok(()),
5581 };
5582 self.pos += 2;
5583 attrs.parallel = level;
5584 }
5585 "cost" | "rows" => {
5586 let Some(n) = self.peek_number_at(self.pos + 1) else {
5587 return Ok(());
5588 };
5589 self.pos += 2;
5590 if word == "cost" {
5591 attrs.cost = Some(n);
5592 } else {
5593 attrs.rows = Some(n);
5594 }
5595 }
5596 _ => return Ok(()),
5597 }
5598 }
5599 }
5600
5601 /// The numeric literal at `idx`, if there is one.
5602 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5603 match self.tokens.get(idx)? {
5604 Token::Integer(n) => Some(*n as f64),
5605 Token::Float(f) => Some(*f),
5606 Token::Numeric(t) => t.parse::<f64>().ok(),
5607 _ => None,
5608 }
5609 }
5610
5611 /// Closing `)`-terminated argument list. v7.12.4 commonly
5612 /// sees the empty `()`; typed args round-trip but the
5613 /// executor (yet) doesn't invoke them.
5614 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5615 /// it away, which is what PG does with one on a function parameter.
5616 fn skip_type_modifier(&mut self) {
5617 if !matches!(self.peek(), Token::LParen) {
5618 return;
5619 }
5620 // Only a numeric modifier — anything else is not one, and eating
5621 // it would swallow real grammar.
5622 let mut i = self.pos + 1;
5623 let mut seen_number = false;
5624 loop {
5625 match self.tokens.get(i) {
5626 Some(Token::Integer(_)) => seen_number = true,
5627 Some(Token::Comma) => {}
5628 Some(Token::RParen) => break,
5629 _ => return,
5630 }
5631 i += 1;
5632 }
5633 if !seen_number {
5634 return;
5635 }
5636 while self.pos <= i {
5637 self.advance();
5638 }
5639 }
5640
5641 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5642 let mut args: Vec<FunctionArg> = Vec::new();
5643 if matches!(self.peek(), Token::RParen) {
5644 self.advance();
5645 return Ok(args);
5646 }
5647 loop {
5648 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5649 // a reserved token; OUT / INOUT are bare idents.
5650 let mode = if matches!(self.peek(), Token::In) {
5651 self.advance();
5652 FunctionArgMode::In
5653 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5654 {
5655 self.advance();
5656 FunctionArgMode::Out
5657 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5658 {
5659 self.advance();
5660 FunctionArgMode::InOut
5661 } else {
5662 FunctionArgMode::In
5663 };
5664 // Optional name. The next token is either a name
5665 // (followed by a type ident) or the type itself.
5666 // Disambiguate by peeking ahead: if the token after
5667 // the next ident is also an ident, we treat the
5668 // first as the name.
5669 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5670 // the comma or paren, then decide. Reading at most two of
5671 // them could not spell `x double precision` at all, and
5672 // silently mis-read the bare `double precision` as a
5673 // parameter named "double" — which is what made the same
5674 // signature key two different ways.
5675 let (name, ty_token) = {
5676 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5677 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5678 words.push(self.expect_ident_like()?);
5679 }
5680 // v7.39 (round 344) — a length / precision modifier on the
5681 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5682 // accepts it and DROPS it — `pg_get_function_arguments`
5683 // reports plain `character varying` / `numeric`, measured on
5684 // 18.4 — but SPG raised `syntax error at or near "("`,
5685 // because the modifier's parens were never consumed.
5686 self.skip_type_modifier();
5687 // r1049 — `f(v bigint[])`. The array suffix parsed in
5688 // the column position, the cast position and (r1038)
5689 // the RETURNS position, but not here: the fifth
5690 // member of the same family, reported by sentori as
5691 // presumably the same code. It is now.
5692 let array_suffix = self.consume_array_suffix();
5693 let whole = words.join(" ");
5694 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5695 {
5696 (Some(words[0].clone()), words[1..].join(" "))
5697 } else {
5698 (None, whole)
5699 };
5700 ty_token.push_str(&array_suffix);
5701 (name, ty_token)
5702 };
5703 // Type — try to map to ColumnTypeName, else Raw.
5704 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5705 Some(t) => FunctionArgType::Typed(t),
5706 None => FunctionArgType::Raw(ty_token),
5707 };
5708 args.push(FunctionArg { mode, name, ty });
5709 match self.peek() {
5710 Token::Comma => {
5711 self.advance();
5712 continue;
5713 }
5714 Token::RParen => {
5715 self.advance();
5716 return Ok(args);
5717 }
5718 other => {
5719 return Err(self.err(alloc::format!(
5720 "expected , or ) in function arg list, got {other:?}"
5721 )));
5722 }
5723 }
5724 }
5725 }
5726
5727 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5728 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5729 // function whose row shape is named inline.
5730 if matches!(self.peek(), Token::Table)
5731 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5732 {
5733 self.advance(); // TABLE
5734 self.advance(); // (
5735 let mut cols: Vec<String> = Vec::new();
5736 loop {
5737 let cname = self.expect_ident_like()?;
5738 let mut ty: Vec<String> = Vec::new();
5739 loop {
5740 match self.peek() {
5741 Token::Comma | Token::RParen | Token::Eof => break,
5742 _ => {}
5743 }
5744 match self.advance() {
5745 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5746 other => {
5747 if let Some(w) = unreserved_keyword_text(&other) {
5748 ty.push(w);
5749 }
5750 }
5751 }
5752 }
5753 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5754 if matches!(self.peek(), Token::Comma) {
5755 self.advance();
5756 } else {
5757 break;
5758 }
5759 }
5760 if matches!(self.peek(), Token::RParen) {
5761 self.advance();
5762 }
5763 return Ok(FunctionReturn::Other(alloc::format!(
5764 "TABLE({})",
5765 cols.join(", ")
5766 )));
5767 }
5768 let ident = self.expect_ident_like()?;
5769 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5770 if ident.eq_ignore_ascii_case("setof") {
5771 let inner = self.expect_ident_like()?;
5772 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5773 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5774 }
5775 if ident.eq_ignore_ascii_case("trigger") {
5776 return Ok(FunctionReturn::Trigger);
5777 }
5778 if ident.eq_ignore_ascii_case("void") {
5779 return Ok(FunctionReturn::Void);
5780 }
5781 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5782 // RETURN position did not, so the `[` was a syntax error and the
5783 // whole migration stopped. sentori worked around it by returning
5784 // zero-padded text.
5785 let suffix = self.consume_array_suffix();
5786 if !suffix.is_empty() {
5787 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5788 }
5789 match map_type_ident_to_column_type_name(&ident) {
5790 Some(t) => Ok(FunctionReturn::Type(t)),
5791 None => Ok(FunctionReturn::Other(ident)),
5792 }
5793 }
5794
5795 /// Consume any `[]` / `[N]` array markers after a type name and give
5796 /// back their text. Empty when there are none.
5797 fn consume_array_suffix(&mut self) -> String {
5798 let mut out = String::new();
5799 while matches!(self.peek(), Token::LBracket) {
5800 self.advance();
5801 // `[N]` is accepted and, as in PG, the length is not enforced.
5802 if let Token::Integer(n) = self.peek().clone() {
5803 self.advance();
5804 out.push_str(&alloc::format!("[{n}]"));
5805 } else {
5806 out.push_str("[]");
5807 }
5808 if matches!(self.peek(), Token::RBracket) {
5809 self.advance();
5810 }
5811 }
5812 out
5813 }
5814
5815 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5816 match self.peek() {
5817 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5818 self.advance();
5819 let lang = self.expect_ident_like()?;
5820 Ok(Some(lang.to_ascii_lowercase()))
5821 }
5822 _ => Ok(None),
5823 }
5824 }
5825
5826 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5827 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5828 /// (expr)]*`. The `DOMAIN` keyword has already been
5829 /// consumed. PG allows the trailing constraints in any
5830 /// order; we approximate with a small loop.
5831 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5832 let name = self.expect_ident_like()?;
5833 // Optional `AS`.
5834 if matches!(self.peek(), Token::As) {
5835 self.advance();
5836 }
5837 // v7.39 (round 259) — keep the raw type NAME when the base is not
5838 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5839 // parent domain.
5840 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5841 self.parse_type_with_implied_flags()?;
5842 let mut default: Option<Expr> = None;
5843 let mut not_null = false;
5844 let mut checks: Vec<Expr> = Vec::new();
5845 loop {
5846 match self.peek() {
5847 Token::Default => {
5848 if default.is_some() {
5849 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5850 }
5851 self.advance();
5852 default = Some(self.parse_expr(0)?);
5853 }
5854 Token::Not => {
5855 self.advance();
5856 if !matches!(self.peek(), Token::Null) {
5857 return Err(self.err(alloc::format!(
5858 "expected NULL after NOT in DOMAIN, got {:?}",
5859 self.peek()
5860 )));
5861 }
5862 self.advance();
5863 not_null = true;
5864 }
5865 Token::Null => {
5866 self.advance();
5867 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5868 // is the default-nullable marker (PG accepts it),
5869 // but AFTER a NOT NULL it is a conflict PG refuses
5870 // (`conflicting NULL/NOT NULL constraints`,
5871 // PG18-measured); the old arm no-opped both ways.
5872 if not_null {
5873 return Err(self.err(alloc::string::String::from(
5874 "conflicting NULL/NOT NULL constraints",
5875 )));
5876 }
5877 }
5878 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5879 self.advance();
5880 if !matches!(self.peek(), Token::LParen) {
5881 return Err(self.err(alloc::format!(
5882 "expected '(' after CHECK in DOMAIN, got {:?}",
5883 self.peek()
5884 )));
5885 }
5886 self.advance();
5887 let expr = self.parse_expr(0)?;
5888 if !matches!(self.peek(), Token::RParen) {
5889 return Err(self.err(alloc::format!(
5890 "expected ')' after CHECK expr, got {:?}",
5891 self.peek()
5892 )));
5893 }
5894 self.advance();
5895 checks.push(expr);
5896 }
5897 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5898 // prefix on the constraint; we drop the name and
5899 // recurse into the constraint parsing.
5900 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5901 self.advance();
5902 let _ = self.expect_ident_like()?;
5903 }
5904 _ => break,
5905 }
5906 }
5907 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5908 name,
5909 base_type,
5910 base_domain: base_user_ref,
5911 default,
5912 not_null,
5913 checks,
5914 }))
5915 }
5916
5917 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5918 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5919 /// consumed.
5920 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5921 let name = self.expect_ident_like()?;
5922 // Required `AS`.
5923 if !matches!(self.peek(), Token::As) {
5924 return Err(self.err(alloc::format!(
5925 "expected AS after CREATE TYPE {name:?}, got {:?}",
5926 self.peek()
5927 )));
5928 }
5929 self.advance();
5930 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5931 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5932 // on the next token: `(` = composite, ident `ENUM` = enum.
5933 if matches!(self.peek(), Token::LParen) {
5934 self.advance();
5935 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5936 let mut field_user_types: Vec<Option<String>> = Vec::new();
5937 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5938 // is legal PG (an attribute-less composite; measured — the old
5939 // e2e note claimed PG requires at least one attribute).
5940 if matches!(self.peek(), Token::RParen) {
5941 self.advance();
5942 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5943 name,
5944 kind: crate::ast::TypeKind::Composite {
5945 fields,
5946 field_user_types,
5947 },
5948 }));
5949 }
5950 loop {
5951 let field_name = self.expect_ident_like()?;
5952 // v7.39 (round 264) — keep the raw type name when it is not
5953 // a builtin: that is how a NESTED composite field records
5954 // which composite it holds.
5955 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5956 self.parse_type_with_implied_flags()?;
5957 fields.push((field_name, field_type));
5958 field_user_types.push(field_user_ref);
5959 if matches!(self.peek(), Token::Comma) {
5960 self.advance();
5961 continue;
5962 }
5963 if matches!(self.peek(), Token::RParen) {
5964 self.advance();
5965 break;
5966 }
5967 return Err(self.err(alloc::format!(
5968 "expected , or ) in composite field list, got {:?}",
5969 self.peek()
5970 )));
5971 }
5972 if fields.is_empty() {
5973 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5974 }
5975 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5976 name,
5977 kind: crate::ast::TypeKind::Composite {
5978 fields,
5979 field_user_types,
5980 },
5981 }));
5982 }
5983 // Required `ENUM` ident.
5984 let kind_ident = match self.peek().clone() {
5985 Token::Ident(s) | Token::QuotedIdent(s) => s,
5986 other => {
5987 return Err(self.err(alloc::format!(
5988 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5989 )));
5990 }
5991 };
5992 if !kind_ident.eq_ignore_ascii_case("enum") {
5993 return Err(self.err(alloc::format!(
5994 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5995 )));
5996 }
5997 self.advance();
5998 if !matches!(self.peek(), Token::LParen) {
5999 return Err(self.err(alloc::format!(
6000 "expected '(' after ENUM, got {:?}",
6001 self.peek()
6002 )));
6003 }
6004 self.advance();
6005 let mut labels: Vec<String> = Vec::new();
6006 loop {
6007 match self.peek().clone() {
6008 Token::String(s) => {
6009 self.advance();
6010 labels.push(s);
6011 }
6012 other => {
6013 return Err(
6014 self.err(alloc::format!("expected enum label string, got {other:?}"))
6015 );
6016 }
6017 }
6018 if matches!(self.peek(), Token::Comma) {
6019 self.advance();
6020 continue;
6021 }
6022 if matches!(self.peek(), Token::RParen) {
6023 self.advance();
6024 break;
6025 }
6026 return Err(self.err(alloc::format!(
6027 "expected , or ) in ENUM label list, got {:?}",
6028 self.peek()
6029 )));
6030 }
6031 if labels.is_empty() {
6032 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6033 }
6034 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6035 name,
6036 kind: crate::ast::TypeKind::Enum { labels },
6037 }))
6038 }
6039
6040 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6041 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6042 /// The `CREATE MATERIALIZED VIEW` keywords have already been
6043 /// consumed.
6044 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6045 let if_not_exists = self.parse_if_not_exists();
6046 let name = self.expect_ident_like()?;
6047 let mut columns: Vec<String> = Vec::new();
6048 if matches!(self.peek(), Token::LParen) {
6049 self.advance();
6050 loop {
6051 let c = self.expect_ident_like()?;
6052 columns.push(c);
6053 if matches!(self.peek(), Token::Comma) {
6054 self.advance();
6055 continue;
6056 }
6057 if matches!(self.peek(), Token::RParen) {
6058 self.advance();
6059 break;
6060 }
6061 return Err(self.err(alloc::format!(
6062 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6063 self.peek()
6064 )));
6065 }
6066 }
6067 if !matches!(self.peek(), Token::As) {
6068 return Err(self.err(alloc::format!(
6069 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6070 self.peek()
6071 )));
6072 }
6073 self.advance();
6074 // v7.39 (round 151) — a WITH-headed body is legal (read-only
6075 // CTEs only; the engine rejects data-modifying ones with PG's
6076 // message). A trailing `WITH [NO] DATA` can't START the body,
6077 // so WITH here heads the query.
6078 let body = if self.peek_is_with_kw() {
6079 self.advance();
6080 self.parse_nested_with_select()?
6081 } else {
6082 let body_stmt = self.parse_select_stmt()?;
6083 let Statement::Select(body) = body_stmt else {
6084 return Err(self.err(alloc::format!(
6085 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6086 )));
6087 };
6088 body
6089 };
6090 // Optional trailing `WITH [NO] DATA`.
6091 let with_data = self.parse_optional_with_data(true)?;
6092 Ok(Statement::CreateMaterializedView(
6093 crate::ast::CreateMaterializedViewStatement {
6094 temporary: false,
6095 name,
6096 if_not_exists,
6097 columns,
6098 body,
6099 with_data,
6100 as_plain_table: false,
6101 },
6102 ))
6103 }
6104
6105 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6106 /// `default_when_absent` is what to return if the tail is
6107 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6108 /// WITH DATA).
6109 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6110 let save = self.pos;
6111 // `WITH` is an Ident (not reserved in the lexer).
6112 let is_with = match self.peek() {
6113 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6114 _ => false,
6115 };
6116 if !is_with {
6117 return Ok(default_when_absent);
6118 }
6119 self.advance();
6120 // Optional `NO`.
6121 let mut with_data = true;
6122 let is_no = match self.peek() {
6123 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6124 _ => false,
6125 };
6126 if is_no {
6127 self.advance();
6128 with_data = false;
6129 }
6130 // Required `DATA` ident.
6131 let is_data = match self.peek() {
6132 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6133 _ => false,
6134 };
6135 if is_data {
6136 self.advance();
6137 Ok(with_data)
6138 } else {
6139 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6140 // parser can interpret it.
6141 self.pos = save;
6142 Ok(default_when_absent)
6143 }
6144 }
6145
6146 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6147 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6148 /// All keyword prefixes have already been consumed; the flags
6149 /// say which were present.
6150 fn parse_create_view_after_keyword(
6151 &mut self,
6152 or_replace: bool,
6153 _materialized_unused: bool,
6154 temporary: bool,
6155 ) -> Result<Statement, ParseError> {
6156 let if_not_exists = self.parse_if_not_exists();
6157 let name = self.expect_ident_like()?;
6158 // Optional `(col, col, …)` rename list.
6159 let mut columns: Vec<String> = Vec::new();
6160 if matches!(self.peek(), Token::LParen) {
6161 self.advance();
6162 loop {
6163 let c = self.expect_ident_like()?;
6164 columns.push(c);
6165 if matches!(self.peek(), Token::Comma) {
6166 self.advance();
6167 continue;
6168 }
6169 if matches!(self.peek(), Token::RParen) {
6170 self.advance();
6171 break;
6172 }
6173 return Err(self.err(alloc::format!(
6174 "expected , or ) in VIEW column list, got {:?}",
6175 self.peek()
6176 )));
6177 }
6178 }
6179 // Required `AS`.
6180 if !matches!(self.peek(), Token::As) {
6181 return Err(self.err(alloc::format!(
6182 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6183 self.peek()
6184 )));
6185 }
6186 self.advance();
6187 // Body: a regular SELECT statement. v7.39 (round 151) — a
6188 // WITH-headed body is legal too (read-only CTEs only; the
6189 // engine rejects data-modifying ones with PG's message).
6190 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6191 // with the check-option clause, so WITH here heads the query.
6192 let body = if self.peek_is_with_kw() {
6193 self.advance();
6194 self.parse_nested_with_select()?
6195 } else {
6196 let body_stmt = self.parse_select_stmt()?;
6197 let Statement::Select(body) = body_stmt else {
6198 return Err(self.err(alloc::format!(
6199 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6200 )));
6201 };
6202 body
6203 };
6204 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6205 // The SELECT parser stops before a trailing WITH, so it lands here.
6206 let check_option = if matches!(self.peek(),
6207 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6208 {
6209 self.advance(); // WITH
6210 let opt = match self.peek() {
6211 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6212 self.advance();
6213 crate::ast::ViewCheckOption::Local
6214 }
6215 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6216 self.advance();
6217 crate::ast::ViewCheckOption::Cascaded
6218 }
6219 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6220 _ => crate::ast::ViewCheckOption::Cascaded,
6221 };
6222 if !matches!(self.peek(),
6223 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6224 {
6225 return Err(self.err(alloc::format!(
6226 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6227 self.peek()
6228 )));
6229 }
6230 self.advance(); // CHECK
6231 if !matches!(self.peek(),
6232 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6233 {
6234 return Err(self.err(alloc::format!(
6235 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6236 self.peek()
6237 )));
6238 }
6239 self.advance(); // OPTION
6240 Some(opt)
6241 } else {
6242 None
6243 };
6244 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6245 name,
6246 or_replace,
6247 if_not_exists,
6248 temporary,
6249 columns,
6250 body,
6251 check_option,
6252 }))
6253 }
6254
6255 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6256 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6257 /// consumed; `temporary` carries whether TEMPORARY was seen.
6258 fn parse_create_sequence_after_keyword(
6259 &mut self,
6260 temporary: bool,
6261 ) -> Result<Statement, ParseError> {
6262 let if_not_exists = self.parse_if_not_exists();
6263 let name = self.expect_ident_like()?;
6264 // Optional `AS data_type`.
6265 let data_type = if matches!(self.peek(), Token::As) {
6266 self.advance();
6267 Some(self.parse_sequence_data_type()?)
6268 } else {
6269 None
6270 };
6271 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6272 Ok(Statement::CreateSequence(
6273 crate::ast::CreateSequenceStatement {
6274 name,
6275 if_not_exists,
6276 temporary,
6277 data_type,
6278 options,
6279 },
6280 ))
6281 }
6282
6283 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6284 /// already been consumed; this is reached after `SEQUENCE`.
6285 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6286 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6287 use crate::ast::AlterDomainAction as A;
6288 let name = self.expect_ident_like()?;
6289 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6290 let kw = match self.peek() {
6291 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6292 Token::Drop => alloc::string::String::from("drop"),
6293 Token::Default => alloc::string::String::from("default"),
6294 other => {
6295 return Err(self.err(alloc::format!(
6296 "expected an ALTER DOMAIN action, got {other:?}"
6297 )));
6298 }
6299 };
6300 let action = match kw.as_str() {
6301 "add" => {
6302 self.advance();
6303 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6304 {
6305 self.advance();
6306 Some(self.expect_ident_like()?)
6307 } else {
6308 None
6309 };
6310 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6311 return Err(self.err(alloc::format!(
6312 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6313 self.peek()
6314 )));
6315 }
6316 self.advance();
6317 if !matches!(self.peek(), Token::LParen) {
6318 return Err(self.err("expected '(' after CHECK".into()));
6319 }
6320 self.advance();
6321 let check = self.parse_expr(0)?;
6322 if !matches!(self.peek(), Token::RParen) {
6323 return Err(self.err("expected ')' after CHECK expression".into()));
6324 }
6325 self.advance();
6326 A::AddConstraint { name: cname, check }
6327 }
6328 "drop" => {
6329 self.advance();
6330 match self.peek() {
6331 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6332 self.advance();
6333 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6334 {
6335 self.advance();
6336 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6337 {
6338 return Err(self.err("expected EXISTS after IF".into()));
6339 }
6340 self.advance();
6341 true
6342 } else {
6343 false
6344 };
6345 let cn = self.expect_ident_like()?;
6346 A::DropConstraint {
6347 name: cn,
6348 if_exists,
6349 }
6350 }
6351 Token::Default => {
6352 self.advance();
6353 A::DropDefault
6354 }
6355 Token::Not => {
6356 self.advance();
6357 if !matches!(self.peek(), Token::Null) {
6358 return Err(self.err("expected NULL after NOT".into()));
6359 }
6360 self.advance();
6361 A::DropNotNull
6362 }
6363 other => {
6364 return Err(self.err(alloc::format!(
6365 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6366 )));
6367 }
6368 }
6369 }
6370 "set" => {
6371 self.advance();
6372 match self.peek() {
6373 Token::Default => {
6374 self.advance();
6375 A::SetDefault(self.parse_expr(0)?)
6376 }
6377 Token::Not => {
6378 self.advance();
6379 if !matches!(self.peek(), Token::Null) {
6380 return Err(self.err("expected NULL after NOT".into()));
6381 }
6382 self.advance();
6383 A::SetNotNull
6384 }
6385 other => {
6386 return Err(self.err(alloc::format!(
6387 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6388 )));
6389 }
6390 }
6391 }
6392 "rename" => {
6393 self.advance();
6394 if !matches!(self.peek(), Token::To) {
6395 return Err(self.err("expected TO after RENAME".into()));
6396 }
6397 self.advance();
6398 A::RenameTo(self.expect_ident_like()?)
6399 }
6400 other => {
6401 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6402 }
6403 };
6404 Ok(Statement::AlterDomain { name, action })
6405 }
6406
6407 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6408 let if_exists = self.parse_if_exists();
6409 let name = self.expect_ident_like()?;
6410 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6411 // the option list (PG allows only one or the other).
6412 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6413 self.advance();
6414 if matches!(self.peek(), Token::To) {
6415 self.advance();
6416 } else {
6417 self.expect_keyword_ident("to")?;
6418 }
6419 let new = self.expect_ident_like()?;
6420 return Ok(Statement::AlterSequence(
6421 crate::ast::AlterSequenceStatement {
6422 name,
6423 if_exists,
6424 options: crate::ast::SequenceOptions::default(),
6425 rename_to: Some(new),
6426 },
6427 ));
6428 }
6429 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6430 Ok(Statement::AlterSequence(
6431 crate::ast::AlterSequenceStatement {
6432 name,
6433 if_exists,
6434 options,
6435 rename_to: None,
6436 },
6437 ))
6438 }
6439
6440 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6441 let kw = self.expect_ident_like()?;
6442 match kw.to_ascii_lowercase().as_str() {
6443 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6444 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6445 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6446 other => Err(self.err(alloc::format!(
6447 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6448 ))),
6449 }
6450 }
6451
6452 fn parse_sequence_options(
6453 &mut self,
6454 allow_restart: bool,
6455 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6456 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6457 let mut opts = SequenceOptions::default();
6458 #[allow(clippy::while_let_loop)]
6459 loop {
6460 // Match an ident; stop at any non-ident token (sentinel,
6461 // semicolon, end of statement).
6462 let kw_lc = match self.peek() {
6463 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6464 _ => break,
6465 };
6466 match kw_lc.as_str() {
6467 "increment" => {
6468 self.advance();
6469 // Optional BY.
6470 if self.peek_is_by() {
6471 self.advance();
6472 }
6473 opts.increment = Some(self.expect_signed_int()?);
6474 }
6475 "minvalue" => {
6476 self.advance();
6477 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6478 }
6479 "maxvalue" => {
6480 self.advance();
6481 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6482 }
6483 "no" => {
6484 self.advance();
6485 let what = self.expect_ident_like()?;
6486 match what.to_ascii_lowercase().as_str() {
6487 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6488 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6489 "cycle" => opts.cycle = Some(false),
6490 other => {
6491 return Err(self.err(alloc::format!(
6492 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6493 )));
6494 }
6495 }
6496 }
6497 "start" => {
6498 self.advance();
6499 // Optional WITH.
6500 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6501 if s.eq_ignore_ascii_case("with"))
6502 {
6503 self.advance();
6504 }
6505 opts.start = Some(self.expect_signed_int()?);
6506 }
6507 "restart" if allow_restart => {
6508 self.advance();
6509 // Optional WITH n; bare RESTART means restart at START.
6510 let mut with_val: Option<i64> = None;
6511 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6512 if s.eq_ignore_ascii_case("with"))
6513 {
6514 self.advance();
6515 with_val = Some(self.expect_signed_int()?);
6516 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6517 with_val = Some(self.expect_signed_int()?);
6518 }
6519 opts.restart = Some(with_val);
6520 }
6521 "cache" => {
6522 self.advance();
6523 opts.cache = Some(self.expect_signed_int()?);
6524 }
6525 "cycle" => {
6526 self.advance();
6527 opts.cycle = Some(true);
6528 }
6529 "owned" => {
6530 self.advance();
6531 match self.peek() {
6532 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6533 self.advance();
6534 }
6535 other => {
6536 return Err(
6537 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6538 );
6539 }
6540 }
6541 // OWNED BY {NONE | tab.col}. Read just one ident
6542 // (NOT expect_ident_like which would auto-strip
6543 // a schema prefix and consume the `.col` we need).
6544 let first = match self.advance() {
6545 Token::Ident(s) | Token::QuotedIdent(s) => s,
6546 other => {
6547 return Err(self.err(alloc::format!(
6548 "expected identifier or NONE after OWNED BY, got {other:?}"
6549 )));
6550 }
6551 };
6552 if first.eq_ignore_ascii_case("none") {
6553 opts.owned_by = Some(SequenceOwnedBy::None);
6554 } else if matches!(self.peek(), Token::Dot) {
6555 self.advance();
6556 let second = match self.advance() {
6557 Token::Ident(s) | Token::QuotedIdent(s) => s,
6558 other => {
6559 return Err(self.err(alloc::format!(
6560 "expected column name after OWNED BY {first}., got {other:?}"
6561 )));
6562 }
6563 };
6564 // v7.17 dump-compat fix — pg_dump emits
6565 // OWNED BY clauses as
6566 // `schema.table.column` (three segments).
6567 // If a third `.<ident>` follows, treat the
6568 // first ident as schema (drop it; SPG is
6569 // single-schema) and the middle / last
6570 // pair as table.column. Otherwise it's
6571 // the two-segment form table.column.
6572 if matches!(self.peek(), Token::Dot) {
6573 self.advance();
6574 let third = match self.advance() {
6575 Token::Ident(s) | Token::QuotedIdent(s) => s,
6576 other => {
6577 return Err(self.err(alloc::format!(
6578 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6579 )));
6580 }
6581 };
6582 let _ = first; // schema prefix discarded
6583 opts.owned_by = Some(SequenceOwnedBy::Column {
6584 table: second,
6585 column: third,
6586 });
6587 } else {
6588 opts.owned_by = Some(SequenceOwnedBy::Column {
6589 table: first,
6590 column: second,
6591 });
6592 }
6593 } else {
6594 return Err(self.err(alloc::format!(
6595 "expected table.column or NONE after OWNED BY, got {first:?}"
6596 )));
6597 }
6598 }
6599 _ => break,
6600 }
6601 }
6602 Ok(opts)
6603 }
6604
6605 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6606 let neg = if matches!(self.peek(), Token::Minus) {
6607 self.advance();
6608 true
6609 } else {
6610 false
6611 };
6612 match self.peek() {
6613 Token::Integer(n) => {
6614 let v = *n;
6615 self.advance();
6616 Ok(if neg { -v } else { v })
6617 }
6618 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6619 }
6620 }
6621
6622 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6623 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6624 /// clause is fully accepted and discarded — SPG always runs
6625 /// constraint checks immediately (single-writer model). The
6626 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6627 /// in either order (per the SQL spec they're independent),
6628 /// though pg_dump always emits them in the canonical
6629 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6630 /// Stops at the first token that isn't part of the clause.
6631 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6632 self.consume_deferrable_clauses_timed().map(|_| ())
6633 }
6634
6635 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6636 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6637 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6638 /// NOT DEFERRABLE and a circular-FK migration could not load.
6639 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6640 let mut deferrable = false;
6641 let mut initially_deferred = false;
6642 loop {
6643 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6644 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6645 self.advance();
6646 deferrable = true;
6647 if self.consume_optional_initially_clause()? {
6648 initially_deferred = true;
6649 }
6650 continue;
6651 }
6652 // `NOT DEFERRABLE` — already worked pre-3.1.
6653 if matches!(self.peek(), Token::Not) {
6654 let look = self.tokens.get(self.pos + 1);
6655 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6656 self.advance(); // NOT
6657 self.advance(); // DEFERRABLE
6658 deferrable = false;
6659 initially_deferred = false;
6660 let _ = self.consume_optional_initially_clause()?;
6661 continue;
6662 }
6663 break;
6664 }
6665 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6666 // accepts this without a leading [NOT] DEFERRABLE
6667 // (the timing keyword alone). pg_dump occasionally
6668 // emits it on FK constraints that inherit timing.
6669 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6670 if self.consume_optional_initially_clause()? {
6671 initially_deferred = true;
6672 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6673 deferrable = true;
6674 }
6675 continue;
6676 }
6677 break;
6678 }
6679 Ok((deferrable, initially_deferred))
6680 }
6681
6682 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6683 /// next token is `INITIALLY`, consume it plus the required
6684 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6685 /// Returns true when the timing seen was `DEFERRED`.
6686 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6687 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6688 return Ok(false);
6689 }
6690 self.advance(); // INITIALLY
6691 match self.advance() {
6692 Token::Ident(s)
6693 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6694 {
6695 Ok(s.eq_ignore_ascii_case("deferred"))
6696 }
6697 other => Err(self.err(alloc::format!(
6698 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6699 ))),
6700 }
6701 }
6702
6703 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6704 /// in its entirety so the parser returns Empty without
6705 /// touching the runtime. The CREATE+PROCEDURE keywords are
6706 /// already consumed; this swallows everything from the
6707 /// procedure name through the matching `END`, including
6708 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6709 /// (DELIMITER `//` makes the script splitter forward the
6710 /// whole block as one statement), `@var` session-variable
6711 /// references, and the trailing terminator.
6712 ///
6713 /// Tracks nesting depth so:
6714 /// BEGIN
6715 /// IF cond THEN
6716 /// BEGIN ... END;
6717 /// END IF;
6718 /// END
6719 /// terminates at the outer END.
6720 fn consume_mysql_routine_body(&mut self) {
6721 // Outer skeleton: name, (...), optional clauses, BEGIN
6722 // <body> END [;]. Scan for the first BEGIN — anything
6723 // before it is signature decoration we don't care about.
6724 // Once inside BEGIN, count up on BEGIN, down on END.
6725 let mut depth: i32 = 0;
6726 let mut started = false;
6727 loop {
6728 match self.peek().clone() {
6729 Token::Begin => {
6730 self.advance();
6731 depth += 1;
6732 started = true;
6733 }
6734 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6735 self.advance();
6736 if started {
6737 depth -= 1;
6738 if depth <= 0 {
6739 // Optional trailing ident (`END IF`,
6740 // `END LOOP`, `END WHILE`, `END CASE`,
6741 // `END label_name`) — eat the next
6742 // ident if present so we don't
6743 // mistake `END IF;` for the outer
6744 // close.
6745 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6746 // If the next token is one of the
6747 // PL/SQL block-closer keywords,
6748 // the END belongs to an inner
6749 // block; bump depth back up.
6750 let is_inner_close = matches!(
6751 self.peek(),
6752 Token::Ident(s) | Token::QuotedIdent(s)
6753 if matches!(
6754 s.to_ascii_lowercase().as_str(),
6755 "if" | "loop" | "while" | "case" | "repeat"
6756 )
6757 );
6758 if is_inner_close {
6759 self.advance();
6760 depth += 1;
6761 continue;
6762 }
6763 }
6764 // Eat optional trailing `;`.
6765 if matches!(self.peek(), Token::Semicolon) {
6766 self.advance();
6767 }
6768 return;
6769 }
6770 }
6771 }
6772 Token::Eof => return,
6773 _ => {
6774 self.advance();
6775 }
6776 }
6777 }
6778 }
6779
6780 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6781 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6782 ///
6783 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6784 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6785 /// ident, or `ident @ ident-or-quoted-string` host form)
6786 /// * `SQL SECURITY {DEFINER|INVOKER}`
6787 ///
6788 /// Each clause may appear at most once but in any order.
6789 /// The hints are pure planner / permission metadata that
6790 /// SPG's view-rewrite engine handles uniformly; we accept
6791 /// and discard. Returns `Ok(())` once a non-clause token is
6792 /// peeked (the caller then checks for the `VIEW` keyword).
6793 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6794 loop {
6795 match self.peek().clone() {
6796 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6797 self.advance(); // ALGORITHM
6798 // Optional `=`. MySQL spec requires it but be
6799 // generous.
6800 if matches!(self.peek(), Token::Eq) {
6801 self.advance();
6802 }
6803 // UNDEFINED / MERGE / TEMPTABLE — accept any
6804 // bare ident; unknown values still parse so
6805 // future MySQL versions don't break.
6806 if matches!(
6807 self.peek(),
6808 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6809 ) {
6810 self.advance();
6811 }
6812 }
6813 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6814 self.advance(); // DEFINER
6815 if matches!(self.peek(), Token::Eq) {
6816 self.advance();
6817 }
6818 // User: quoted string, ident, OR ident @ host
6819 // (host may itself be quoted or bare).
6820 match self.peek().clone() {
6821 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6822 self.advance();
6823 // Optional `@host`.
6824 if matches!(self.peek(), Token::At) {
6825 self.advance();
6826 if matches!(
6827 self.peek(),
6828 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6829 ) {
6830 self.advance();
6831 }
6832 }
6833 }
6834 _ => {}
6835 }
6836 }
6837 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6838 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6839 // when followed by SECURITY — the dispatcher must
6840 // not consume a bare `SQL` token (it's not a
6841 // legal CREATE prefix on its own).
6842 let save = self.pos;
6843 self.advance(); // SQL
6844 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6845 if s2.eq_ignore_ascii_case("security"))
6846 {
6847 self.advance(); // SECURITY
6848 // DEFINER / INVOKER trailing ident.
6849 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6850 self.advance();
6851 }
6852 } else {
6853 // Not a SQL SECURITY clause — roll back and
6854 // bail; the caller will error out cleanly.
6855 self.pos = save;
6856 return Ok(());
6857 }
6858 }
6859 _ => return Ok(()),
6860 }
6861 }
6862 }
6863
6864 fn parse_if_not_exists(&mut self) -> bool {
6865 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6866 {
6867 let save = self.pos;
6868 self.advance();
6869 if matches!(self.peek(), Token::Not) {
6870 self.advance();
6871 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6872 {
6873 self.advance();
6874 return true;
6875 }
6876 }
6877 self.pos = save;
6878 }
6879 false
6880 }
6881
6882 fn parse_if_exists(&mut self) -> bool {
6883 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6884 {
6885 let save = self.pos;
6886 self.advance();
6887 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6888 {
6889 self.advance();
6890 return true;
6891 }
6892 self.pos = save;
6893 }
6894 false
6895 }
6896
6897 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6898 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6899 /// been consumed.
6900 fn parse_create_trigger_after_keyword(
6901 &mut self,
6902 or_replace: bool,
6903 ) -> Result<Statement, ParseError> {
6904 let name = self.expect_ident_like()?;
6905 let timing = {
6906 let ident = self.expect_ident_like()?;
6907 if ident.eq_ignore_ascii_case("before") {
6908 TriggerTiming::Before
6909 } else if ident.eq_ignore_ascii_case("after") {
6910 TriggerTiming::After
6911 } else if ident.eq_ignore_ascii_case("instead") {
6912 let next = self.expect_ident_like()?;
6913 if !next.eq_ignore_ascii_case("of") {
6914 return Err(self.err(alloc::format!(
6915 "expected OF after INSTEAD in trigger timing, got {next:?}"
6916 )));
6917 }
6918 TriggerTiming::InsteadOf
6919 } else {
6920 return Err(self.err(alloc::format!(
6921 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6922 )));
6923 }
6924 };
6925 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6926 // OR is a reserved keyword token (Token::Or), not an Ident.
6927 // v7.13.0 — after an UPDATE event we may optionally see
6928 // `OF col, col, …` (mailrs round-5 G7). Columns are
6929 // captured into `update_columns` once across the whole
6930 // events list; multiple `UPDATE OF` clauses are rejected.
6931 let mut events: Vec<TriggerEvent> = Vec::new();
6932 let mut update_columns: Vec<String> = Vec::new();
6933 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6934 events.push(first_ev);
6935 if !first_cols.is_empty() {
6936 update_columns = first_cols;
6937 }
6938 while matches!(self.peek(), Token::Or) {
6939 self.advance();
6940 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6941 events.push(ev);
6942 if !cols.is_empty() {
6943 if !update_columns.is_empty() {
6944 return Err(
6945 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6946 );
6947 }
6948 update_columns = cols;
6949 }
6950 }
6951 // ON <table>
6952 let tok = self.peek();
6953 let Token::On = tok else {
6954 return Err(self.err(alloc::format!(
6955 "expected ON after trigger events, got {tok:?}"
6956 )));
6957 };
6958 self.advance();
6959 let table = self.expect_ident_like()?;
6960 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6961 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6962 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6963 // the trigger as a plain AFTER trigger (correct for every non-deferred
6964 // use; deferral timing is not yet honoured).
6965 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6966 if s.eq_ignore_ascii_case("from"))
6967 {
6968 self.advance();
6969 let _reftable = self.expect_ident_like()?;
6970 }
6971 self.consume_optional_deferrable_clauses()?;
6972 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6973 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6974 // idents.
6975 if !matches!(self.peek(), Token::For) {
6976 return Err(self.err(alloc::format!(
6977 "expected FOR EACH ROW / STATEMENT, got {:?}",
6978 self.peek()
6979 )));
6980 }
6981 self.advance();
6982 let for_each = {
6983 let e = self.expect_ident_like()?;
6984 if !e.eq_ignore_ascii_case("each") {
6985 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6986 }
6987 let unit = self.expect_ident_like()?;
6988 if unit.eq_ignore_ascii_case("row") {
6989 TriggerForEach::Row
6990 } else if unit.eq_ignore_ascii_case("statement") {
6991 TriggerForEach::Statement
6992 } else {
6993 return Err(self.err(alloc::format!(
6994 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6995 )));
6996 }
6997 };
6998 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6999 let when_condition = if matches!(self.peek(),
7000 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7001 {
7002 self.advance();
7003 Some(self.parse_paren_expr("WHEN")?)
7004 } else {
7005 None
7006 };
7007 // EXECUTE FUNCTION/PROCEDURE name(...)
7008 let exec = self.expect_ident_like()?;
7009 if !exec.eq_ignore_ascii_case("execute") {
7010 return Err(self.err(alloc::format!(
7011 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7012 )));
7013 }
7014 let fn_or_proc = self.expect_ident_like()?;
7015 if !(fn_or_proc.eq_ignore_ascii_case("function")
7016 || fn_or_proc.eq_ignore_ascii_case("procedure"))
7017 {
7018 return Err(self.err(alloc::format!(
7019 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7020 )));
7021 }
7022 let function = self.expect_ident_like()?;
7023 // Optional empty arg list `()`.
7024 if matches!(self.peek(), Token::LParen) {
7025 self.advance();
7026 if !matches!(self.peek(), Token::RParen) {
7027 return Err(self.err(alloc::format!(
7028 "v7.12.4 trigger function calls take no args; got {:?}",
7029 self.peek()
7030 )));
7031 }
7032 self.advance();
7033 }
7034 Ok(Statement::CreateTrigger(CreateTriggerStatement {
7035 name,
7036 or_replace,
7037 timing,
7038 events,
7039 table,
7040 for_each,
7041 function,
7042 update_columns,
7043 when_condition,
7044 }))
7045 }
7046
7047 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7048 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7049 fn parse_create_rule_after_keyword(
7050 &mut self,
7051 or_replace: bool,
7052 ) -> Result<Statement, ParseError> {
7053 let name = self.expect_ident_like()?;
7054 if !matches!(self.peek(), Token::As) {
7055 return Err(self.err(alloc::format!(
7056 "expected AS in CREATE RULE, got {:?}",
7057 self.peek()
7058 )));
7059 }
7060 self.advance();
7061 if !matches!(self.peek(), Token::On) {
7062 return Err(self.err(alloc::format!(
7063 "expected ON in CREATE RULE, got {:?}",
7064 self.peek()
7065 )));
7066 }
7067 self.advance();
7068 let event = self.parse_rule_event()?;
7069 if !matches!(self.peek(), Token::To)
7070 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7071 {
7072 return Err(self.err(alloc::format!(
7073 "expected TO after rule event, got {:?}",
7074 self.peek()
7075 )));
7076 }
7077 self.advance();
7078 let table = self.expect_ident_like()?;
7079 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7080 let when_condition = if matches!(self.peek(), Token::Where) {
7081 self.advance();
7082 Some(self.parse_expr(0)?)
7083 } else {
7084 None
7085 };
7086 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7087 {
7088 return Err(self.err(alloc::format!(
7089 "expected DO in CREATE RULE, got {:?}",
7090 self.peek()
7091 )));
7092 }
7093 self.advance();
7094 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7095 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7096 {
7097 self.advance();
7098 true
7099 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7100 self.advance();
7101 false
7102 } else {
7103 false
7104 };
7105 // `NOTHING` | `( cmd; … )` | `cmd`.
7106 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7107 {
7108 self.advance();
7109 Vec::new()
7110 } else if matches!(self.peek(), Token::LParen) {
7111 self.advance();
7112 let mut cmds = Vec::new();
7113 loop {
7114 cmds.push(self.parse_one_statement()?);
7115 if matches!(self.peek(), Token::Semicolon) {
7116 self.advance();
7117 if matches!(self.peek(), Token::RParen) {
7118 break;
7119 }
7120 continue;
7121 }
7122 break;
7123 }
7124 if !matches!(self.peek(), Token::RParen) {
7125 return Err(self.err(alloc::format!(
7126 "expected ) closing the CREATE RULE command list, got {:?}",
7127 self.peek()
7128 )));
7129 }
7130 self.advance();
7131 cmds
7132 } else {
7133 alloc::vec![self.parse_one_statement()?]
7134 };
7135 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7136 name,
7137 or_replace,
7138 event,
7139 table,
7140 instead,
7141 when_condition,
7142 commands,
7143 }))
7144 }
7145
7146 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7147 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7148 if matches!(self.peek(), Token::Insert) {
7149 self.advance();
7150 return Ok(alloc::string::String::from("INSERT"));
7151 }
7152 if matches!(self.peek(), Token::Select) {
7153 self.advance();
7154 return Ok(alloc::string::String::from("SELECT"));
7155 }
7156 match self.peek() {
7157 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7158 self.advance();
7159 Ok(alloc::string::String::from("UPDATE"))
7160 }
7161 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7162 self.advance();
7163 Ok(alloc::string::String::from("DELETE"))
7164 }
7165 other => Err(self.err(alloc::format!(
7166 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7167 ))),
7168 }
7169 }
7170
7171 /// v7.13.0 — parse one trigger event, then optionally consume
7172 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7173 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7174 fn parse_trigger_event_with_optional_of(
7175 &mut self,
7176 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7177 let ev = self.parse_trigger_event()?;
7178 if !matches!(ev, TriggerEvent::Update) {
7179 return Ok((ev, Vec::new()));
7180 }
7181 // `OF` is a bare ident.
7182 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7183 return Ok((ev, Vec::new()));
7184 }
7185 self.advance(); // OF
7186 let mut cols: Vec<String> = Vec::new();
7187 loop {
7188 cols.push(self.expect_ident_like()?);
7189 if matches!(self.peek(), Token::Comma) {
7190 self.advance();
7191 continue;
7192 }
7193 break;
7194 }
7195 if cols.is_empty() {
7196 return Err(
7197 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7198 );
7199 }
7200 Ok((ev, cols))
7201 }
7202
7203 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7204 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7205 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7206 /// inside the body.
7207 /// Called by [`parse_plpgsql_body`] after the body's tokens
7208 /// have been lexed into this temporary parser.
7209 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7210 // v7.12.6 — optional DECLARE prelude.
7211 let declarations = if matches!(
7212 self.peek(),
7213 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7214 ) {
7215 self.advance();
7216 self.parse_plpgsql_declare_block()?
7217 } else {
7218 Vec::new()
7219 };
7220 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7221 // `BEGIN` transaction-start, but we can reuse the
7222 // reserved Token::Begin since the body is a separate
7223 // lex/parse context).
7224 if !matches!(self.peek(), Token::Begin) {
7225 return Err(self.err(alloc::format!(
7226 "expected BEGIN at start of plpgsql block, got {:?}",
7227 self.peek()
7228 )));
7229 }
7230 self.advance();
7231 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7232 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7233 // body's last statement and the trailing END. When present
7234 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7235 // arms terminated by END.
7236 let exception_handlers = if matches!(
7237 self.peek(),
7238 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7239 ) {
7240 self.advance();
7241 self.parse_plpgsql_exception_handlers()?
7242 } else {
7243 Vec::new()
7244 };
7245 Ok(PlPgSqlBlock {
7246 declarations,
7247 statements,
7248 exception_handlers,
7249 })
7250 }
7251
7252 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7253 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7254 fn parse_plpgsql_exception_handlers(
7255 &mut self,
7256 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7257 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7258 loop {
7259 // Stop at END — the block-level trailing END LOOP / END;
7260 // is handled by the caller.
7261 if matches!(
7262 self.peek(),
7263 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7264 ) {
7265 return Ok(out);
7266 }
7267 // WHEN <cond> [OR <cond>]* THEN <body>
7268 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7269 {
7270 return Err(self.err(alloc::format!(
7271 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7272 self.peek()
7273 )));
7274 }
7275 self.advance();
7276 let mut conditions: Vec<String> = Vec::new();
7277 conditions.push(self.expect_ident_like()?);
7278 while matches!(self.peek(), Token::Or) {
7279 self.advance();
7280 conditions.push(self.expect_ident_like()?);
7281 }
7282 let then_kw = self.expect_ident_like()?;
7283 if !then_kw.eq_ignore_ascii_case("then") {
7284 return Err(self.err(alloc::format!(
7285 "expected THEN after WHEN condition list, got {then_kw:?}"
7286 )));
7287 }
7288 let body = self.parse_plpgsql_stmt_list_until_end()?;
7289 out.push(crate::ast::ExceptionHandler { conditions, body });
7290 }
7291 }
7292
7293 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7294 /// prelude. Caller has already consumed `DECLARE`. We stop
7295 /// reading entries when we hit `BEGIN`.
7296 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7297 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7298 loop {
7299 if matches!(self.peek(), Token::Begin) {
7300 return Ok(out);
7301 }
7302 let name = self.expect_ident_like()?;
7303 // v7.37.20 (20.7) — type inference: if the next token is
7304 // `:=` or `=` (no explicit type), infer from the default
7305 // expression. Otherwise the ident that follows is the
7306 // declared type.
7307 //
7308 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7309 // (PG-standard). SPG parse-accepts and treats identically
7310 // to inference — the eventual runtime value determines
7311 // the local's type, which is faithful to how SPG handles
7312 // untyped locals today (see 20.7). Full compile-time
7313 // catalog lookup queues with v7.40 PL/pgSQL epic.
7314 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7315 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7316 // downstream declaration walker to type the local by
7317 // the runtime type of the default expression.
7318 FunctionArgType::Raw("_infer_".into())
7319 } else {
7320 let ty_token = self.expect_ident_like()?;
7321 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7322 // consume optional `.<ident>` qualifier + `%<KW>`
7323 // suffix. Both qualifier and suffix map to _infer_.
7324 if matches!(self.peek(), Token::Dot) {
7325 self.advance();
7326 let _ = self.expect_ident_like()?;
7327 }
7328 if matches!(self.peek(), Token::Percent) {
7329 self.advance();
7330 // Consume the trailing TYPE / ROWTYPE ident.
7331 let _ = self.expect_ident_like()?;
7332 FunctionArgType::Raw("_infer_".into())
7333 } else {
7334 match map_type_ident_to_column_type_name(&ty_token) {
7335 Some(t) => FunctionArgType::Typed(t),
7336 None => FunctionArgType::Raw(ty_token),
7337 }
7338 }
7339 };
7340 let default = match self.peek() {
7341 Token::ColonEq => {
7342 self.advance();
7343 Some(self.parse_expr(0)?)
7344 }
7345 Token::Eq => {
7346 // PL/pgSQL also accepts `=` for the
7347 // DECLARE default (PG treats them the same
7348 // in this position).
7349 self.advance();
7350 Some(self.parse_expr(0)?)
7351 }
7352 _ => None,
7353 };
7354 // Mandatory `;` between declarations.
7355 if !matches!(self.peek(), Token::Semicolon) {
7356 return Err(self.err(alloc::format!(
7357 "expected ; after DECLARE entry for {name:?}, got {:?}",
7358 self.peek()
7359 )));
7360 }
7361 self.advance();
7362 out.push(PlPgSqlDeclare { name, ty, default });
7363 }
7364 }
7365
7366 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7367 /// the terminating `END;` (or `END IF;` etc — handled by the
7368 /// per-construct sub-parsers). Used by both the outer block
7369 /// and the IF/ELSE branch bodies.
7370 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7371 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7372 loop {
7373 // Allow trailing semicolons + END.
7374 while matches!(self.peek(), Token::Semicolon) {
7375 self.advance();
7376 }
7377 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7378 if matches!(
7379 self.peek(),
7380 Token::Ident(s) | Token::QuotedIdent(s)
7381 if s.eq_ignore_ascii_case("end")
7382 || s.eq_ignore_ascii_case("else")
7383 || s.eq_ignore_ascii_case("elsif")
7384 || s.eq_ignore_ascii_case("elseif")
7385 || s.eq_ignore_ascii_case("exception")
7386 || s.eq_ignore_ascii_case("when")
7387 ) {
7388 return Ok(statements);
7389 }
7390 // Otherwise: one statement, then expect `;` or
7391 // a block-terminator keyword.
7392 let stmt = self.parse_plpgsql_stmt()?;
7393 statements.push(stmt);
7394 match self.peek() {
7395 Token::Semicolon => {
7396 self.advance();
7397 }
7398 Token::Ident(s) | Token::QuotedIdent(s)
7399 if s.eq_ignore_ascii_case("end")
7400 || s.eq_ignore_ascii_case("else")
7401 || s.eq_ignore_ascii_case("elsif")
7402 || s.eq_ignore_ascii_case("elseif")
7403 || s.eq_ignore_ascii_case("exception")
7404 || s.eq_ignore_ascii_case("when") =>
7405 {
7406 // Final statement of the block without `;`.
7407 }
7408 other => {
7409 return Err(self.err(alloc::format!(
7410 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7411 )));
7412 }
7413 }
7414 }
7415 }
7416
7417 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7418 // RETURN keyword?
7419 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7420 {
7421 self.advance();
7422 return self.parse_plpgsql_return();
7423 }
7424 // v7.12.6 — IF block.
7425 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7426 {
7427 self.advance();
7428 return self.parse_plpgsql_if();
7429 }
7430 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7431 // Detected by peeking that token pos+3 is Ident("execute").
7432 if matches!(self.peek(), Token::For)
7433 && matches!(
7434 self.tokens.get(self.pos + 1),
7435 Some(Token::Ident(_) | Token::QuotedIdent(_))
7436 )
7437 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7438 && matches!(
7439 self.tokens.get(self.pos + 3),
7440 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7441 )
7442 {
7443 self.advance(); // FOR
7444 let var = self.expect_ident_like()?;
7445 self.advance(); // IN
7446 self.advance(); // EXECUTE
7447 // Prescan for LOOP at paren depth 0 so parse_expr stops
7448 // before the LOOP keyword (same trick as the bare-SELECT
7449 // ForQuery arm).
7450 let mut depth: i32 = 0;
7451 let mut loop_pos: Option<usize> = None;
7452 let mut scan = self.pos;
7453 while scan < self.tokens.len() {
7454 match self.tokens.get(scan) {
7455 Some(Token::LParen) => depth += 1,
7456 Some(Token::RParen) => depth -= 1,
7457 Some(Token::Ident(s) | Token::QuotedIdent(s))
7458 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7459 {
7460 loop_pos = Some(scan);
7461 break;
7462 }
7463 _ => {}
7464 }
7465 scan += 1;
7466 }
7467 let loop_pos = loop_pos.ok_or_else(|| {
7468 self.err(alloc::format!(
7469 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7470 ))
7471 })?;
7472 let saved_loop = self.tokens[loop_pos].clone();
7473 self.tokens[loop_pos] = Token::Semicolon;
7474 let expr_result = self.parse_expr(0);
7475 self.tokens[loop_pos] = saved_loop;
7476 let sql_expr = expr_result?;
7477 let loop_kw = self.expect_ident_like()?;
7478 if !loop_kw.eq_ignore_ascii_case("loop") {
7479 return Err(self.err(alloc::format!(
7480 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7481 )));
7482 }
7483 let body = self.parse_plpgsql_stmt_list_until_end()?;
7484 let end_kw = self.expect_ident_like()?;
7485 if !end_kw.eq_ignore_ascii_case("end") {
7486 return Err(self.err(alloc::format!(
7487 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7488 )));
7489 }
7490 let loop_kw2 = self.expect_ident_like()?;
7491 if !loop_kw2.eq_ignore_ascii_case("loop") {
7492 return Err(self.err(alloc::format!(
7493 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7494 )));
7495 }
7496 return Ok(PlPgSqlStmt::ForExecute {
7497 var,
7498 sql_expr,
7499 body,
7500 });
7501 }
7502 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7503 //
7504 // Two syntactic forms:
7505 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7506 // FOR var IN (SELECT ...) LOOP ...
7507 //
7508 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7509 // the trailing `LOOP` keyword as a table alias, we prescan
7510 // forward to find LOOP at paren depth 0, splice a fake
7511 // Semicolon at that position (so SELECT parses cleanly),
7512 // then re-splice LOOP back in.
7513 //
7514 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7515 // LOOP directly — no scan required.
7516 if matches!(self.peek(), Token::For)
7517 && matches!(
7518 self.tokens.get(self.pos + 1),
7519 Some(Token::Ident(_) | Token::QuotedIdent(_))
7520 )
7521 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7522 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7523 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7524 {
7525 self.advance(); // FOR
7526 let var = self.expect_ident_like()?;
7527 // IN
7528 self.advance();
7529 let query = if matches!(self.peek(), Token::LParen) {
7530 // Paren-wrapped SELECT.
7531 self.advance();
7532 let inner = self.parse_select_stmt()?;
7533 let Statement::Select(q) = inner else {
7534 return Err(self.err(alloc::format!(
7535 "expected SELECT inside (…), got {:?}",
7536 self.peek()
7537 )));
7538 };
7539 if !matches!(self.peek(), Token::RParen) {
7540 return Err(self.err(alloc::format!(
7541 "expected ')' after FOR-IN-SELECT body, got {:?}",
7542 self.peek()
7543 )));
7544 }
7545 self.advance();
7546 q
7547 } else {
7548 // Bare SELECT: prescan to find the LOOP boundary.
7549 let mut depth: i32 = 0;
7550 let mut loop_pos: Option<usize> = None;
7551 let mut scan = self.pos;
7552 while scan < self.tokens.len() {
7553 match self.tokens.get(scan) {
7554 Some(Token::LParen) => depth += 1,
7555 Some(Token::RParen) => depth -= 1,
7556 Some(Token::Ident(s) | Token::QuotedIdent(s))
7557 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7558 {
7559 loop_pos = Some(scan);
7560 break;
7561 }
7562 _ => {}
7563 }
7564 scan += 1;
7565 }
7566 let loop_pos = loop_pos.ok_or_else(|| {
7567 self.err(alloc::format!(
7568 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7569 ))
7570 })?;
7571 // Swap the LOOP token with a synthetic Semicolon so
7572 // parse_select_stmt stops there, then restore afterward.
7573 let saved_loop = self.tokens[loop_pos].clone();
7574 self.tokens[loop_pos] = Token::Semicolon;
7575 let parse_result = self.parse_select_stmt();
7576 self.tokens[loop_pos] = saved_loop;
7577 let inner = parse_result?;
7578 let Statement::Select(q) = inner else {
7579 return Err(self.err(alloc::format!(
7580 "expected SELECT after FOR <var> IN, got {:?}",
7581 self.peek()
7582 )));
7583 };
7584 q
7585 };
7586 let loop_kw = self.expect_ident_like()?;
7587 if !loop_kw.eq_ignore_ascii_case("loop") {
7588 return Err(self.err(alloc::format!(
7589 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7590 )));
7591 }
7592 let body = self.parse_plpgsql_stmt_list_until_end()?;
7593 let end_kw = self.expect_ident_like()?;
7594 if !end_kw.eq_ignore_ascii_case("end") {
7595 return Err(self.err(alloc::format!(
7596 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7597 )));
7598 }
7599 let loop_kw2 = self.expect_ident_like()?;
7600 if !loop_kw2.eq_ignore_ascii_case("loop") {
7601 return Err(self.err(alloc::format!(
7602 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7603 )));
7604 }
7605 return Ok(PlPgSqlStmt::ForQuery {
7606 var,
7607 query: Box::new(query),
7608 body,
7609 });
7610 }
7611 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7612 // FOR is a reserved keyword token (Token::For).
7613 if matches!(self.peek(), Token::For)
7614 && matches!(
7615 self.tokens.get(self.pos + 1),
7616 Some(Token::Ident(_) | Token::QuotedIdent(_))
7617 )
7618 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7619 {
7620 self.advance(); // FOR
7621 let var = self.expect_ident_like()?;
7622 if !matches!(self.peek(), Token::In) {
7623 return Err(self.err(alloc::format!(
7624 "expected IN after FOR <var>, got {:?}",
7625 self.peek()
7626 )));
7627 }
7628 self.advance();
7629 let reverse = matches!(
7630 self.peek(),
7631 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7632 );
7633 if reverse {
7634 self.advance();
7635 }
7636 let start = self.parse_expr(0)?;
7637 if !matches!(self.peek(), Token::DotDot) {
7638 return Err(self.err(alloc::format!(
7639 "expected '..' between FOR loop bounds, got {:?}",
7640 self.peek()
7641 )));
7642 }
7643 self.advance();
7644 let end = self.parse_expr(0)?;
7645 let loop_kw = self.expect_ident_like()?;
7646 if !loop_kw.eq_ignore_ascii_case("loop") {
7647 return Err(self.err(alloc::format!(
7648 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7649 )));
7650 }
7651 let body = self.parse_plpgsql_stmt_list_until_end()?;
7652 let end_kw = self.expect_ident_like()?;
7653 if !end_kw.eq_ignore_ascii_case("end") {
7654 return Err(self.err(alloc::format!(
7655 "expected END LOOP after FOR body, got {end_kw:?}"
7656 )));
7657 }
7658 let loop_kw2 = self.expect_ident_like()?;
7659 if !loop_kw2.eq_ignore_ascii_case("loop") {
7660 return Err(self.err(alloc::format!(
7661 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7662 )));
7663 }
7664 return Ok(PlPgSqlStmt::ForRange {
7665 var,
7666 start,
7667 end,
7668 reverse,
7669 body,
7670 });
7671 }
7672 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7673 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7674 {
7675 self.advance();
7676 let body = self.parse_plpgsql_stmt_list_until_end()?;
7677 let end_kw = self.expect_ident_like()?;
7678 if !end_kw.eq_ignore_ascii_case("end") {
7679 return Err(self.err(alloc::format!(
7680 "expected END LOOP after LOOP body, got {end_kw:?}"
7681 )));
7682 }
7683 let loop_kw = self.expect_ident_like()?;
7684 if !loop_kw.eq_ignore_ascii_case("loop") {
7685 return Err(self.err(alloc::format!(
7686 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7687 )));
7688 }
7689 return Ok(PlPgSqlStmt::Loop { body });
7690 }
7691 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7692 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7693 {
7694 self.advance();
7695 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7696 {
7697 self.advance();
7698 Some(self.parse_expr(0)?)
7699 } else {
7700 None
7701 };
7702 return Ok(PlPgSqlStmt::Exit { when });
7703 }
7704 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7705 // already-parsed Statement or a runtime-computed SQL string.
7706 // The disambiguator vs the extended-query-protocol `EXECUTE
7707 // <stmt_name>` (which is a top-level Statement, not a
7708 // plpgsql line) is that inside a DO block / trigger body the
7709 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7710 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7711 {
7712 self.advance();
7713 let sql = self.parse_expr(0)?;
7714 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7715 }
7716 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7717 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7718 {
7719 self.advance();
7720 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7721 {
7722 self.advance();
7723 Some(self.parse_expr(0)?)
7724 } else {
7725 None
7726 };
7727 return Ok(PlPgSqlStmt::Continue { when });
7728 }
7729 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7730 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7731 {
7732 self.advance();
7733 let condition = self.parse_expr(0)?;
7734 let loop_kw = self.expect_ident_like()?;
7735 if !loop_kw.eq_ignore_ascii_case("loop") {
7736 return Err(self.err(alloc::format!(
7737 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7738 )));
7739 }
7740 let body = self.parse_plpgsql_stmt_list_until_end()?;
7741 // Expect END LOOP.
7742 let end_kw = self.expect_ident_like()?;
7743 if !end_kw.eq_ignore_ascii_case("end") {
7744 return Err(self.err(alloc::format!(
7745 "expected END LOOP after WHILE body, got {end_kw:?}"
7746 )));
7747 }
7748 let loop_kw2 = self.expect_ident_like()?;
7749 if !loop_kw2.eq_ignore_ascii_case("loop") {
7750 return Err(self.err(alloc::format!(
7751 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7752 )));
7753 }
7754 return Ok(PlPgSqlStmt::While { condition, body });
7755 }
7756 // v7.12.6 — RAISE.
7757 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7758 {
7759 self.advance();
7760 return self.parse_plpgsql_raise();
7761 }
7762 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7763 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7764 {
7765 self.advance();
7766 let condition = self.parse_expr(0)?;
7767 let message = if matches!(self.peek(), Token::Comma) {
7768 self.advance();
7769 Some(self.parse_expr(0)?)
7770 } else {
7771 None
7772 };
7773 return Ok(PlPgSqlStmt::Assert { condition, message });
7774 }
7775 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7776 // "PERFORM is equivalent to SELECT but discards the
7777 // result." Side effects (function calls, RAISE inside
7778 // SQL functions, etc.) still execute. We desugar to
7779 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7780 // existing embedded-statement path handles execution +
7781 // result-discard cleanly. The result is naturally
7782 // discarded because EmbeddedSql doesn't propagate row
7783 // sets back to the plpgsql interpreter.
7784 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7785 {
7786 self.advance();
7787 // Splice a synthetic Token::Select into the stream at
7788 // the current position so parse_select_stmt parses the
7789 // remainder as a normal SELECT body. Token-stream
7790 // surgery mirrors the try_parse_plpgsql_select_into
7791 // pattern used for SELECT … INTO desugaring.
7792 self.tokens.insert(self.pos, Token::Select);
7793 let select = self.parse_select_stmt()?;
7794 let Statement::Select(s) = select else {
7795 return Err(self.err(alloc::format!(
7796 "expected SELECT body after PERFORM, got {:?}",
7797 self.peek()
7798 )));
7799 };
7800 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7801 }
7802 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7803 // plpgsql-specific shape (mailrs round-10 migrate-042).
7804 // PG's SELECT INTO at top-level SQL would CREATE a new
7805 // table; inside plpgsql it ASSIGNS the query result to
7806 // a local variable. We detect the INTO at paren-depth
7807 // 0 between SELECT and the statement boundary; if
7808 // found, split the token stream into "pre-INTO
7809 // projection" + "var" + "post-INTO FROM/WHERE…" and
7810 // rebuild as a SelectInto with a regular SELECT body
7811 // (no INTO clause).
7812 if matches!(self.peek(), Token::Select)
7813 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7814 {
7815 return Ok(PlPgSqlStmt::SelectInto {
7816 var: var_name,
7817 body: Box::new(select_body),
7818 });
7819 }
7820 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7821 // SELECT can appear directly inside a trigger body; we
7822 // recurse into the regular Statement parser, which will
7823 // stop at the trailing `;` (which our caller then
7824 // consumes).
7825 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7826 // also embed ALTER / CREATE / DROP statements; route
7827 // those through the same parser so the DO body parses
7828 // cleanly.
7829 if matches!(self.peek(), Token::Insert)
7830 || matches!(self.peek(), Token::Select)
7831 || matches!(self.peek(), Token::Create)
7832 || matches!(self.peek(), Token::Drop)
7833 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7834 if s.eq_ignore_ascii_case("update")
7835 || s.eq_ignore_ascii_case("delete")
7836 || s.eq_ignore_ascii_case("alter"))
7837 {
7838 let stmt = self.parse_one_statement()?;
7839 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7840 }
7841 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7842 // followed by `:=` and an expression.
7843 let target = self.parse_plpgsql_assign_target()?;
7844 // PL/pgSQL assignment uses `:=`. The lexer represents
7845 // this as a colon followed by `=`; check both shapes.
7846 match self.peek() {
7847 Token::ColonEq => {
7848 self.advance();
7849 }
7850 Token::Colon => {
7851 self.advance();
7852 if !matches!(self.peek(), Token::Eq) {
7853 return Err(self.err(alloc::format!(
7854 "expected := after plpgsql assign target, got `:` then {:?}",
7855 self.peek()
7856 )));
7857 }
7858 self.advance();
7859 }
7860 other => {
7861 return Err(self.err(alloc::format!(
7862 "expected := after plpgsql assign target, got {other:?}"
7863 )));
7864 }
7865 }
7866 let value = self.parse_expr(0)?;
7867 Ok(PlPgSqlStmt::Assign { target, value })
7868 }
7869
7870 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7871 /// [ELSE body] END IF`. `IF` keyword already consumed.
7872 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7873 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7874 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7875 loop {
7876 // <expr> THEN
7877 let cond = self.parse_expr(0)?;
7878 let then_kw = self.expect_ident_like()?;
7879 if !then_kw.eq_ignore_ascii_case("then") {
7880 return Err(self.err(alloc::format!(
7881 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7882 )));
7883 }
7884 let body = self.parse_plpgsql_stmt_list_until_end()?;
7885 branches.push((cond, body));
7886 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7887 match self.peek() {
7888 Token::Ident(s) | Token::QuotedIdent(s)
7889 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7890 {
7891 self.advance();
7892 continue;
7893 }
7894 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7895 self.advance();
7896 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7897 break;
7898 }
7899 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7900 break;
7901 }
7902 other => {
7903 return Err(self.err(alloc::format!(
7904 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7905 )));
7906 }
7907 }
7908 }
7909 // Expect `END IF` (the END keyword is the one we're
7910 // looking at right now).
7911 let end_kw = self.expect_ident_like()?;
7912 if !end_kw.eq_ignore_ascii_case("end") {
7913 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7914 }
7915 let if_kw = self.expect_ident_like()?;
7916 if !if_kw.eq_ignore_ascii_case("if") {
7917 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7918 }
7919 Ok(PlPgSqlStmt::If {
7920 branches,
7921 else_branch,
7922 })
7923 }
7924
7925 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7926 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7927 /// is already consumed.
7928 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7929 let lvl_ident = self.expect_ident_like()?;
7930 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7931 "notice" => RaiseLevel::Notice,
7932 "warning" => RaiseLevel::Warning,
7933 "info" => RaiseLevel::Info,
7934 "log" => RaiseLevel::Log,
7935 "debug" => RaiseLevel::Debug,
7936 "exception" => RaiseLevel::Exception,
7937 other => {
7938 return Err(self.err(alloc::format!(
7939 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7940 )));
7941 }
7942 };
7943 // Message: required for v7.12.6. PG accepts a bare
7944 // RAISE-rethrow form (no message), reserved for future
7945 // RAISE-no-args support.
7946 let Token::String(msg) = self.peek() else {
7947 return Err(self.err(alloc::format!(
7948 "expected RAISE message string, got {:?}",
7949 self.peek()
7950 )));
7951 };
7952 let message = msg.clone();
7953 self.advance();
7954 // Optional comma-separated args (PG `%` format substitution).
7955 let mut args: Vec<Expr> = Vec::new();
7956 while matches!(self.peek(), Token::Comma) {
7957 self.advance();
7958 args.push(self.parse_expr(0)?);
7959 }
7960 Ok(PlPgSqlStmt::Raise {
7961 level,
7962 message,
7963 args,
7964 })
7965 }
7966
7967 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7968 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7969 /// migrate-042). Returns `(rebuilt_select_without_into,
7970 /// var_name)` when the pattern matches; `None` for
7971 /// regular SELECTs (those go through the embedded-SQL
7972 /// path). Token-stream surgery so the rebuilt SELECT
7973 /// parses through the regular `parse_select_stmt`.
7974 #[allow(clippy::too_many_lines)]
7975 fn try_parse_plpgsql_select_into(
7976 &mut self,
7977 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7978 // Scan forward from `self.pos + 1` (past Token::Select)
7979 // for Token::Into at paren-depth 0, stopping at the
7980 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7981 // end the plpgsql statement.
7982 let start = self.pos;
7983 let mut into_pos: Option<usize> = None;
7984 let mut depth: i32 = 0;
7985 let mut i = start + 1;
7986 while i < self.tokens.len() {
7987 match &self.tokens[i] {
7988 Token::LParen => depth += 1,
7989 Token::RParen => depth -= 1,
7990 Token::Semicolon if depth == 0 => break,
7991 Token::Ident(s)
7992 if depth == 0
7993 && (s.eq_ignore_ascii_case("end")
7994 || s.eq_ignore_ascii_case("else")
7995 || s.eq_ignore_ascii_case("elsif")) =>
7996 {
7997 break;
7998 }
7999 Token::Into if depth == 0 => {
8000 into_pos = Some(i);
8001 break;
8002 }
8003 _ => {}
8004 }
8005 i += 1;
8006 }
8007 let Some(into_at) = into_pos else {
8008 return Ok(None);
8009 };
8010 // The token immediately after INTO must be the target
8011 // var ident; anything else (e.g. INSERT INTO table)
8012 // ruled out by the depth-0 check above. Capture it.
8013 let var = match self.tokens.get(into_at + 1) {
8014 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8015 other => {
8016 return Err(self.err(alloc::format!(
8017 "expected variable name after SELECT … INTO, got {other:?}"
8018 )));
8019 }
8020 };
8021 // Find the end of the plpgsql SELECT INTO statement —
8022 // same boundary rules as the depth-0 scan above.
8023 let mut end = into_at + 2;
8024 let mut depth2: i32 = 0;
8025 while end < self.tokens.len() {
8026 match &self.tokens[end] {
8027 Token::LParen => depth2 += 1,
8028 Token::RParen => depth2 -= 1,
8029 Token::Semicolon if depth2 == 0 => break,
8030 Token::Ident(s)
8031 if depth2 == 0
8032 && (s.eq_ignore_ascii_case("end")
8033 || s.eq_ignore_ascii_case("else")
8034 || s.eq_ignore_ascii_case("elsif")) =>
8035 {
8036 break;
8037 }
8038 _ => {}
8039 }
8040 end += 1;
8041 }
8042 // Rebuild a token stream that represents the SELECT
8043 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8044 // post-var tokens up to statement end]. Run the
8045 // regular `parse_select_stmt` against it.
8046 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8047 for j in start..into_at {
8048 rebuilt.push(self.tokens[j].clone());
8049 }
8050 for j in (into_at + 2)..end {
8051 rebuilt.push(self.tokens[j].clone());
8052 }
8053 rebuilt.push(Token::Eof);
8054 let saved_pos = self.pos;
8055 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8056 self.pos = 0;
8057 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8058 if !matches!(self.peek(), Token::Select) {
8059 self.tokens = saved_tokens;
8060 self.pos = saved_pos;
8061 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8062 }
8063 let sel = self.parse_select_stmt();
8064 self.tokens = saved_tokens;
8065 self.pos = end;
8066 let sel = sel?;
8067 let Statement::Select(body) = sel else {
8068 return Err(self.err(alloc::format!(
8069 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8070 )));
8071 };
8072 Ok(Some((body, var)))
8073 }
8074
8075 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8076 // v7.16.1 — read the head token DIRECTLY rather than
8077 // via `expect_ident_like`. The v7.14.0 schema-qualifier
8078 // strip (`public.t` → `t`) inside `expect_ident_like`
8079 // greedily consumes any `ident . ident` pair, which
8080 // silently turned every `NEW.col := …` /
8081 // `OLD.col := …` plpgsql assignment into a Local("col")
8082 // assignment — the head "new"/"old" was eaten as if it
8083 // were a schema name and the Dot was consumed too, so
8084 // this function's own `peek() == Token::Dot` check
8085 // below never fired. Every BEFORE trigger that rewrote
8086 // a NEW cell was a silent no-op for two major releases
8087 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8088 // gate failures were investigated as v7.16.1 backlog.
8089 let head = match self.advance() {
8090 Token::Ident(s) | Token::QuotedIdent(s) => s,
8091 other => {
8092 return Err(self.err(alloc::format!(
8093 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8094 )));
8095 }
8096 };
8097 if matches!(self.peek(), Token::Dot) {
8098 self.advance();
8099 let col = self.expect_ident_like()?;
8100 if head.eq_ignore_ascii_case("new") {
8101 return Ok(AssignTarget::NewColumn(col));
8102 }
8103 if head.eq_ignore_ascii_case("old") {
8104 return Ok(AssignTarget::OldColumn(col));
8105 }
8106 return Err(self.err(alloc::format!(
8107 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8108 got {head:?}.<col>"
8109 )));
8110 }
8111 Ok(AssignTarget::Local(head))
8112 }
8113
8114 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8115 // RETURN NEW / OLD / NULL — bare-ident forms.
8116 match self.peek() {
8117 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8118 self.advance();
8119 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8120 }
8121 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8122 self.advance();
8123 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8124 }
8125 Token::Null => {
8126 self.advance();
8127 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8128 }
8129 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8130 // per PL/pgSQL convention.
8131 Token::Semicolon => {
8132 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8133 }
8134 _ => {}
8135 }
8136 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8137 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8138 // caller-visible effect (blocks don't return sets), so we
8139 // desugar it identically to PERFORM: parse the SELECT (or
8140 // EXECUTE dynamic) as embedded SQL that runs for side
8141 // effects and discards the result. RETURN NEXT <expr>
8142 // (single-row accumulator) queues with v7.40 SETOF function
8143 // infrastructure.
8144 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8145 // and keep going.
8146 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8147 {
8148 self.advance();
8149 let e = self.parse_expr(0)?;
8150 return Ok(PlPgSqlStmt::ReturnNext(e));
8151 }
8152 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8153 {
8154 self.advance();
8155 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8156 // rows go to the set, like the static form. It used to desugar to a
8157 // bare ExecuteDynamic, whose result was DISCARDED.
8158 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8159 {
8160 self.advance();
8161 let sql = self.parse_expr(0)?;
8162 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8163 }
8164 // Bare RETURN QUERY <select>. If the current token is
8165 // not already SELECT (e.g., the user wrote `RETURN QUERY
8166 // <projection> FROM ...` in a shorthand — rare but PG
8167 // accepts a bare projection here), splice one in. Same
8168 // trick as PERFORM.
8169 if !matches!(self.peek(), Token::Select) {
8170 self.tokens.insert(self.pos, Token::Select);
8171 }
8172 let select = self.parse_select_stmt()?;
8173 let Statement::Select(s) = select else {
8174 return Err(self.err(alloc::format!(
8175 "expected SELECT body after RETURN QUERY, got {:?}",
8176 self.peek()
8177 )));
8178 };
8179 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8180 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8181 // in a SETOF function is the entire answer thrown away.
8182 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8183 }
8184 // Fall through: parse a full expression.
8185 let e = self.parse_expr(0)?;
8186 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8187 }
8188
8189 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8190 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8191 // are ident-shaped (the parser keys off case-insensitive
8192 // match — same shape used by the top-level Update / Delete
8193 // dispatchers at parse_one_statement).
8194 if matches!(self.peek(), Token::Insert) {
8195 self.advance();
8196 return Ok(TriggerEvent::Insert);
8197 }
8198 match self.peek() {
8199 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8200 self.advance();
8201 Ok(TriggerEvent::Update)
8202 }
8203 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8204 self.advance();
8205 Ok(TriggerEvent::Delete)
8206 }
8207 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8208 self.advance();
8209 Ok(TriggerEvent::Truncate)
8210 }
8211 other => Err(self.err(alloc::format!(
8212 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8213 ))),
8214 }
8215 }
8216
8217 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8218 /// - (no clause) → implicit `FOR ALL TABLES`
8219 /// - `FOR ALL TABLES`
8220 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8221 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8222 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8223 /// REJECTS the bare plural (`invalid publication object list`,
8224 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8225 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8226 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8227 let name = self.expect_ident_or_string()?;
8228 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8229 // shape so existing publications keep parsing identically.
8230 let scope = if matches!(self.peek(), Token::For) {
8231 self.advance();
8232 if matches!(self.peek(), Token::All) {
8233 self.advance();
8234 if !matches!(self.peek(), Token::Tables) {
8235 return Err(self.err(format!(
8236 "expected TABLES after FOR ALL, got {:?}",
8237 self.peek()
8238 )));
8239 }
8240 self.advance();
8241 if matches!(self.peek(), Token::Except) {
8242 self.advance();
8243 let tables = self.parse_publication_table_list()?;
8244 PublicationScope::AllTablesExcept(tables)
8245 } else {
8246 PublicationScope::AllTables
8247 }
8248 } else if matches!(self.peek(), Token::Table) {
8249 self.advance();
8250 let tables = self.parse_publication_table_list()?;
8251 PublicationScope::ForTables(tables)
8252 } else if matches!(self.peek(), Token::Tables) {
8253 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8254 // plural (`FOR TABLES t`) is REJECTED (`invalid
8255 // publication object list`); TABLES only pairs with
8256 // `IN SCHEMA`. The old arm accepted it on an
8257 // unverifiable "PG 19 accepts both" claim.
8258 self.advance();
8259 if !matches!(self.peek(), Token::In) {
8260 return Err(self.err(alloc::string::String::from(
8261 "invalid publication object list",
8262 )));
8263 }
8264 self.advance();
8265 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8266 return Err(self.err(format!(
8267 "expected SCHEMA after FOR TABLES IN, got {:?}",
8268 self.peek()
8269 )));
8270 }
8271 self.advance();
8272 let schema = self.expect_ident_or_string()?;
8273 PublicationScope::TablesInSchema(schema)
8274 } else {
8275 return Err(self.err(format!(
8276 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8277 self.peek()
8278 )));
8279 }
8280 } else {
8281 PublicationScope::AllTables
8282 };
8283 Ok(Statement::CreatePublication(CreatePublicationStatement {
8284 name,
8285 scope,
8286 }))
8287 }
8288
8289 /// v6.1.3 — Comma-separated identifier list for the publication
8290 /// FOR-clause. Requires at least one entry; empty list is a
8291 /// parse error (PG behaviour). Quoted idents are accepted; the
8292 /// names round-trip through `Display` as `quote_ident(name)`.
8293 ///
8294 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8295 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8296 /// pg_dump output. SPG's publication state today is per-table
8297 /// only (matching the pre-PG-15 surface); the col list + WHERE
8298 /// are parsed so dumps load through and the table name reaches
8299 /// `PublicationScope::ForTables`, but the filter is not enforced
8300 /// at publish time. Re-open when a customer dogfood gate
8301 /// requires per-row-filter or column-subset publish semantics
8302 /// (which gates on persistent slot state landing first, 21.12).
8303 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8304 let first = self.parse_publication_table_entry()?;
8305 let mut out = alloc::vec![first];
8306 while matches!(self.peek(), Token::Comma) {
8307 self.advance();
8308 out.push(self.parse_publication_table_entry()?);
8309 }
8310 Ok(out)
8311 }
8312
8313 /// One table entry inside a FOR TABLE clause:
8314 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8315 /// Returns just the table name; the column list + WHERE predicate
8316 /// are consumed and discarded per the parse-accept-discard
8317 /// commitment above.
8318 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8319 let name = self.expect_ident_like()?;
8320 // Optional column list — `(col, col, …)`.
8321 if matches!(self.peek(), Token::LParen) {
8322 self.advance();
8323 // Empty parens are a PG error too; require ≥ 1 column.
8324 let _ = self.expect_ident_like()?;
8325 while matches!(self.peek(), Token::Comma) {
8326 self.advance();
8327 let _ = self.expect_ident_like()?;
8328 }
8329 if !matches!(self.peek(), Token::RParen) {
8330 return Err(self.err(alloc::format!(
8331 "expected ')' to close publication column list, got {:?}",
8332 self.peek()
8333 )));
8334 }
8335 self.advance();
8336 }
8337 // Optional row filter — `WHERE (predicate)`.
8338 if matches!(self.peek(), Token::Where) {
8339 self.advance();
8340 if !matches!(self.peek(), Token::LParen) {
8341 return Err(self.err(alloc::format!(
8342 "expected '(' after WHERE in publication row filter, got {:?}",
8343 self.peek()
8344 )));
8345 }
8346 self.advance();
8347 let _ = self.parse_expr(0)?;
8348 if !matches!(self.peek(), Token::RParen) {
8349 return Err(self.err(alloc::format!(
8350 "expected ')' to close publication WHERE filter, got {:?}",
8351 self.peek()
8352 )));
8353 }
8354 self.advance();
8355 }
8356 Ok(name)
8357 }
8358
8359 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8360 /// CONNECTION '<conn>'
8361 /// PUBLICATION <pub> [, <pub> ...]`.
8362 ///
8363 /// The clause order is fixed (CONNECTION first, then
8364 /// PUBLICATION) to match PG. No WITH-options accepted in
8365 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8366 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8367 let name = self.expect_ident_or_string()?;
8368 if !matches!(self.peek(), Token::Connection) {
8369 return Err(self.err(format!(
8370 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8371 self.peek()
8372 )));
8373 }
8374 self.advance();
8375 let conn_str = self.expect_string_literal()?;
8376 if !matches!(self.peek(), Token::Publication) {
8377 return Err(self.err(format!(
8378 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8379 self.peek()
8380 )));
8381 }
8382 self.advance();
8383 // Reuse the publication FOR-list parser shape: at least one
8384 // identifier, comma-separated.
8385 let first = self.expect_ident_like()?;
8386 let mut publications = alloc::vec![first];
8387 while matches!(self.peek(), Token::Comma) {
8388 self.advance();
8389 publications.push(self.expect_ident_like()?);
8390 }
8391 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8392 name,
8393 conn_str,
8394 publications,
8395 }))
8396 }
8397
8398 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8399 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8400 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8401 /// that fit `u64`.
8402 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8403 /// qualifier is a *namespace* the app owns (`app.user_id`,
8404 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8405 /// to discard. So parse the raw segments here instead of
8406 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8407 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8408 /// a single segment and round-trip unchanged.
8409 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8410 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8411 loop {
8412 let seg = match self.advance() {
8413 Token::Ident(s) | Token::QuotedIdent(s) => s,
8414 other if unreserved_keyword_text(&other).is_some() => {
8415 unreserved_keyword_text(&other).unwrap()
8416 }
8417 other => {
8418 return Err(ParseError {
8419 message: format!("expected parameter name, got {other:?}"),
8420 token_pos: self.consumed_pos(),
8421 });
8422 }
8423 };
8424 parts.push(seg);
8425 if matches!(self.peek(), Token::Dot) {
8426 self.advance();
8427 continue;
8428 }
8429 break;
8430 }
8431 Ok(parts.join(".").to_ascii_lowercase())
8432 }
8433
8434 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8435 Self::parse_set_value_inner(self)
8436 }
8437
8438 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8439 match self.advance() {
8440 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8441 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8442 Ok(crate::ast::SetValue::Default)
8443 }
8444 Token::Ident(s) | Token::QuotedIdent(s) => {
8445 let mut accum = s;
8446 while matches!(self.peek(), Token::Dot) {
8447 self.advance();
8448 let next = self.expect_ident_like()?;
8449 accum.push('.');
8450 accum.push_str(&next);
8451 }
8452 Ok(crate::ast::SetValue::Ident(accum))
8453 }
8454 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8455 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8456 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8457 // spellings that lex as keyword tokens, not idents:
8458 // `SET standard_conforming_strings = on` is in every
8459 // pg_dump preamble (`off` already lexes as an ident).
8460 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8461 // DEFAULT lexes as its keyword token, so the ident arm above
8462 // never saw it and the everyday reset form was a syntax error.
8463 Token::Default => Ok(crate::ast::SetValue::Default),
8464 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8465 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8466 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8467 // v7.14.0 — MySQL session/user variable RHS
8468 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8469 // Wrap as Ident so the SET handler can record it; the
8470 // engine treats `@VAR` / `@@VAR` values as opaque
8471 // strings.
8472 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8473 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8474 // is the common MySQL preamble shape. Allow a `+` or
8475 // `-` prefix on negative numerics for parity with PG
8476 // (some param defaults are negative).
8477 Token::Minus => match self.advance() {
8478 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8479 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8480 other => Err(self.err(format!(
8481 "expected numeric after `-` in SET value, got {other:?}"
8482 ))),
8483 },
8484 other => Err(self.err(format!(
8485 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8486 ))),
8487 }
8488 }
8489
8490 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8491 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8492 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8493 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8494 /// present). Modes are comma-separated per PG; SPG also
8495 /// accepts space-separated for tolerance. READ ONLY / WRITE
8496 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8497 /// surface but not behaviorally honoured today).
8498 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8499 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8500 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8501 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8502 /// session default rather than forcing READ COMMITTED.
8503 fn parse_isolation_level_clauses(
8504 &mut self,
8505 ) -> Result<crate::ast::TransactionModes, ParseError> {
8506 let mut level = IsolationLevel::default();
8507 let mut have_level = false;
8508 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8509 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8510 let mut read_only: Option<bool> = None;
8511 loop {
8512 // ISOLATION LEVEL …
8513 let saw_isolation =
8514 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8515 if saw_isolation {
8516 self.advance(); // ISOLATION
8517 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8518 return Err(self.err(alloc::format!(
8519 "expected LEVEL after ISOLATION, got {:?}",
8520 self.peek()
8521 )));
8522 }
8523 self.advance(); // LEVEL
8524 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8525 let w1 = self
8526 .expect_ident_like()
8527 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8528 let lc = w1.to_ascii_lowercase();
8529 level = match lc.as_str() {
8530 "serializable" => IsolationLevel::Serializable,
8531 "repeatable" => {
8532 // Expect READ
8533 let w2 = self
8534 .expect_ident_like()
8535 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8536 if !w2.eq_ignore_ascii_case("read") {
8537 return Err(self.err(alloc::format!(
8538 "expected READ after REPEATABLE, got {w2:?}"
8539 )));
8540 }
8541 IsolationLevel::RepeatableRead
8542 }
8543 "read" => {
8544 let w2 = self
8545 .expect_ident_like()
8546 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8547 match w2.to_ascii_lowercase().as_str() {
8548 "committed" => IsolationLevel::ReadCommitted,
8549 "uncommitted" => IsolationLevel::ReadUncommitted,
8550 other => {
8551 return Err(self.err(alloc::format!(
8552 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8553 )));
8554 }
8555 }
8556 }
8557 other => {
8558 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8559 }
8560 };
8561 have_level = true;
8562 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8563 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8564 // read "parsed, not behaviorally honoured", and it was
8565 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8566 // opened an ordinary read-write transaction and accepted
8567 // every write in it.
8568 self.advance();
8569 match self.peek().clone() {
8570 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8571 self.advance();
8572 read_only = Some(true);
8573 }
8574 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8575 self.advance();
8576 read_only = Some(false);
8577 }
8578 other => {
8579 return Err(self.err(alloc::format!(
8580 "expected ONLY or WRITE after READ, got {other:?}"
8581 )));
8582 }
8583 }
8584 } else if matches!(self.peek(), Token::Not) {
8585 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8586 self.advance();
8587 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8588 return Err(self.err(alloc::format!(
8589 "expected DEFERRABLE after NOT, got {:?}",
8590 self.peek()
8591 )));
8592 }
8593 self.advance();
8594 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8595 {
8596 self.advance();
8597 } else {
8598 break;
8599 }
8600 // Optional comma between modes.
8601 if matches!(self.peek(), Token::Comma) {
8602 self.advance();
8603 }
8604 }
8605 Ok(crate::ast::TransactionModes {
8606 isolation: have_level.then_some(level),
8607 read_only,
8608 })
8609 }
8610
8611 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8612 // FOR is a v6.1.2-reserved keyword (Token::For). The
8613 // other two are bare idents — they've never needed lexer
8614 // support and we keep it that way.
8615 if !matches!(self.peek(), Token::For) {
8616 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8617 }
8618 self.advance();
8619 self.expect_keyword_ident("wal")?;
8620 self.expect_keyword_ident("position")?;
8621 let pos = self.expect_u64_literal()?;
8622 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8623 {
8624 self.advance();
8625 self.expect_keyword_ident("timeout")?;
8626 Some(self.expect_u64_literal()?)
8627 } else {
8628 None
8629 };
8630 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8631 }
8632
8633 /// v6.1.7 helper — consume a `Token::Integer` and check it
8634 /// fits `u64`. WAL positions and millisecond timeouts are
8635 /// non-negative.
8636 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8637 match self.advance() {
8638 Token::Integer(n) if n >= 0 => Ok(n as u64),
8639 Token::Integer(n) => Err(ParseError {
8640 message: format!("expected non-negative integer, got {n}"),
8641 token_pos: self.consumed_pos(),
8642 }),
8643 other => Err(ParseError {
8644 message: format!("expected integer literal, got {other:?}"),
8645 token_pos: self.consumed_pos(),
8646 }),
8647 }
8648 }
8649
8650 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8651 /// ROLE '<role>' (defaults to readonly). All string slots accept
8652 /// either a quoted ident or a quoted string literal.
8653 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8654 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8655 ///
8656 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8657 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8658 /// wire role) still parses — it is a different axis from the PG attributes.
8659 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8660 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8661 /// or RESET, so the plain attribute forms keep their old path.
8662 fn peeks_db_role_setting(&self) -> bool {
8663 let mut i = self.pos + 1; // past the object's name
8664 let word = |p: usize| -> Option<String> {
8665 match self.tokens.get(p) {
8666 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8667 Some(Token::In) => Some(String::from("in")),
8668 _ => None,
8669 }
8670 };
8671 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8672 i += 3; // IN DATABASE <name>
8673 }
8674 matches!(word(i).as_deref(), Some("set" | "reset"))
8675 }
8676
8677 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8678 use crate::ast::SetDbRoleSettingStatement;
8679 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8680 // identifier, so the ordinary name reader refuses it. Same trap
8681 // as TABLE / INDEX / FULL / DEFAULT before it.
8682 let name = if matches!(self.peek(), Token::All) {
8683 self.advance();
8684 String::from("all")
8685 } else {
8686 self.expect_ident_or_string()?
8687 };
8688 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8689 let all = name.eq_ignore_ascii_case("all");
8690 let (mut database, mut role) = if is_database {
8691 (Some(name), None)
8692 } else if all {
8693 (None, None)
8694 } else {
8695 (None, Some(name))
8696 };
8697 if matches!(self.peek(), Token::In) {
8698 self.advance();
8699 self.advance(); // DATABASE
8700 database = Some(self.expect_ident_or_string()?);
8701 }
8702 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8703 self.advance(); // SET | RESET
8704 if resetting && matches!(self.peek(), Token::All) {
8705 self.advance();
8706 self.consume_until_statement_boundary();
8707 return Ok(Statement::SetDbRoleSetting(Box::new(
8708 SetDbRoleSettingStatement {
8709 database,
8710 role,
8711 param: None,
8712 value: None,
8713 },
8714 )));
8715 }
8716 let param = self.expect_ident_like()?;
8717 let value = if resetting {
8718 None
8719 } else {
8720 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8721 // KEYWORD, so the ident-only check missed it and consumed
8722 // the word itself as the value — the same trap as ALL, one
8723 // clause over.
8724 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8725 self.advance();
8726 }
8727 Some(self.take_guc_value())
8728 };
8729 self.consume_until_statement_boundary();
8730 Ok(Statement::SetDbRoleSetting(Box::new(
8731 SetDbRoleSettingStatement {
8732 database,
8733 role,
8734 param: Some(param),
8735 value,
8736 },
8737 )))
8738 }
8739
8740 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8741 /// a quoted literal loses its quotes, a bare word or number does not.
8742 fn take_guc_value(&mut self) -> String {
8743 match self.advance() {
8744 Token::String(s) => s,
8745 Token::Integer(n) => format!("{n}"),
8746 Token::Float(f) => format!("{f}"),
8747 Token::Ident(s) | Token::QuotedIdent(s) => s,
8748 other => format!("{other:?}"),
8749 }
8750 }
8751
8752 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8753 let name = self.expect_ident_or_string()?;
8754 if self.peek_keyword_ident("with") {
8755 self.advance();
8756 }
8757 let mut password = String::new();
8758 let mut role = String::new();
8759 let mut login: Option<bool> = None;
8760 let mut inherit: Option<bool> = None;
8761 let mut superuser: Option<bool> = None;
8762 // Not a `while let`: the pattern would borrow `self` across the
8763 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8764 #[allow(clippy::while_let_loop)]
8765 loop {
8766 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8767 break;
8768 };
8769 match w.to_ascii_lowercase().as_str() {
8770 "password" => {
8771 self.advance();
8772 password = self.expect_string_literal()?;
8773 }
8774 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8775 // is the same slot.
8776 "encrypted" => {
8777 self.advance();
8778 self.expect_keyword_ident("password")?;
8779 password = self.expect_string_literal()?;
8780 }
8781 "login" => {
8782 self.advance();
8783 login = Some(true);
8784 }
8785 "nologin" => {
8786 self.advance();
8787 login = Some(false);
8788 }
8789 "inherit" => {
8790 self.advance();
8791 inherit = Some(true);
8792 }
8793 "noinherit" => {
8794 self.advance();
8795 inherit = Some(false);
8796 }
8797 "superuser" => {
8798 self.advance();
8799 superuser = Some(true);
8800 }
8801 "nosuperuser" => {
8802 self.advance();
8803 superuser = Some(false);
8804 }
8805 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8806 "role" => {
8807 self.advance();
8808 role = self.expect_string_literal()?;
8809 }
8810 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8811 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8812 // accepted and ignored so a pg_dump role block restores. They
8813 // gate capabilities SPG does not have.
8814 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8815 | "noreplication" | "bypassrls" | "nobypassrls" => {
8816 self.advance();
8817 }
8818 "connection" => {
8819 self.advance();
8820 self.expect_keyword_ident("limit")?;
8821 self.advance(); // the number
8822 }
8823 "valid" => {
8824 self.advance();
8825 self.expect_keyword_ident("until")?;
8826 self.expect_string_literal()?;
8827 }
8828 _ => break,
8829 }
8830 }
8831 if role.is_empty() {
8832 role = "readonly".to_string();
8833 }
8834 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8835 name,
8836 password,
8837 role,
8838 login,
8839 inherit,
8840 superuser,
8841 is_user,
8842 }))
8843 }
8844
8845 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8846 /// consumed the USING / WITH CHECK keyword.
8847 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8848 if !matches!(self.peek(), Token::LParen) {
8849 return Err(self.err(alloc::format!(
8850 "expected '(' after {clause}, got {:?}",
8851 self.peek()
8852 )));
8853 }
8854 self.advance();
8855 let e = self.parse_expr(0)?;
8856 if !matches!(self.peek(), Token::RParen) {
8857 return Err(self.err(alloc::format!(
8858 "expected ')' to close {clause}, got {:?}",
8859 self.peek()
8860 )));
8861 }
8862 self.advance();
8863 Ok(e)
8864 }
8865
8866 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8867 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8868 let mut roles = Vec::new();
8869 loop {
8870 roles.push(self.expect_ident_like()?);
8871 if matches!(self.peek(), Token::Comma) {
8872 self.advance();
8873 } else {
8874 break;
8875 }
8876 }
8877 Ok(roles)
8878 }
8879
8880 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8881 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8882 /// `CREATE POLICY`.
8883 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8884 use crate::ast::PolicyCmd;
8885 let name = self.expect_ident_like()?;
8886 if !matches!(self.peek(), Token::On) {
8887 return Err(self.err(alloc::format!(
8888 "expected ON after CREATE POLICY name, got {:?}",
8889 self.peek()
8890 )));
8891 }
8892 self.advance();
8893 let table = self.expect_ident_like()?;
8894
8895 let mut permissive = true;
8896 if matches!(self.peek(), Token::As) {
8897 self.advance();
8898 let w = self.expect_ident_like()?;
8899 permissive = if w.eq_ignore_ascii_case("permissive") {
8900 true
8901 } else if w.eq_ignore_ascii_case("restrictive") {
8902 false
8903 } else {
8904 return Err(self.err(alloc::format!(
8905 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8906 )));
8907 };
8908 }
8909
8910 let mut cmd = PolicyCmd::All;
8911 if matches!(self.peek(), Token::For) {
8912 self.advance();
8913 cmd = self.parse_policy_cmd()?;
8914 }
8915
8916 let mut roles = Vec::new();
8917 if matches!(self.peek(), Token::To) {
8918 self.advance();
8919 roles = self.parse_policy_roles()?;
8920 }
8921
8922 let mut using = None;
8923 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8924 {
8925 self.advance();
8926 using = Some(self.parse_paren_expr("USING")?);
8927 }
8928
8929 let mut with_check = None;
8930 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8931 {
8932 self.advance();
8933 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8934 {
8935 return Err(self.err(alloc::format!(
8936 "expected CHECK after WITH, got {:?}",
8937 self.peek()
8938 )));
8939 }
8940 self.advance();
8941 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8942 }
8943
8944 // Clause-per-command matrix (PG wording).
8945 match cmd {
8946 PolicyCmd::Insert => {
8947 if using.is_some() {
8948 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8949 }
8950 }
8951 PolicyCmd::Select | PolicyCmd::Delete => {
8952 if with_check.is_some() {
8953 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8954 }
8955 }
8956 PolicyCmd::Update | PolicyCmd::All => {}
8957 }
8958
8959 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8960 name,
8961 table,
8962 permissive,
8963 cmd,
8964 roles,
8965 using,
8966 with_check,
8967 }))
8968 }
8969
8970 /// v7.39 (RLS) — the command word after `FOR`.
8971 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8972 use crate::ast::PolicyCmd;
8973 match self.peek().clone() {
8974 Token::All => {
8975 self.advance();
8976 Ok(PolicyCmd::All)
8977 }
8978 Token::Select => {
8979 self.advance();
8980 Ok(PolicyCmd::Select)
8981 }
8982 Token::Insert => {
8983 self.advance();
8984 Ok(PolicyCmd::Insert)
8985 }
8986 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8987 self.advance();
8988 Ok(PolicyCmd::Update)
8989 }
8990 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8991 self.advance();
8992 Ok(PolicyCmd::Delete)
8993 }
8994 other => Err(self.err(alloc::format!(
8995 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8996 ))),
8997 }
8998 }
8999
9000 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9001 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9002 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9003 let name = self.expect_ident_like()?;
9004 if !matches!(self.peek(), Token::On) {
9005 return Err(self.err(alloc::format!(
9006 "expected ON after ALTER POLICY name, got {:?}",
9007 self.peek()
9008 )));
9009 }
9010 self.advance();
9011 let table = self.expect_ident_like()?;
9012
9013 // RENAME TO new
9014 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9015 {
9016 self.advance();
9017 if !matches!(self.peek(), Token::To) {
9018 return Err(self.err(alloc::format!(
9019 "expected TO after RENAME, got {:?}",
9020 self.peek()
9021 )));
9022 }
9023 self.advance();
9024 let new = self.expect_ident_like()?;
9025 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9026 name,
9027 table,
9028 rename_to: Some(new),
9029 roles: None,
9030 using: None,
9031 with_check: None,
9032 }));
9033 }
9034
9035 let mut roles = None;
9036 if matches!(self.peek(), Token::To) {
9037 self.advance();
9038 roles = Some(self.parse_policy_roles()?);
9039 }
9040 let mut using = None;
9041 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9042 {
9043 self.advance();
9044 using = Some(self.parse_paren_expr("USING")?);
9045 }
9046 let mut with_check = None;
9047 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9048 {
9049 self.advance();
9050 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9051 {
9052 return Err(self.err(alloc::format!(
9053 "expected CHECK after WITH, got {:?}",
9054 self.peek()
9055 )));
9056 }
9057 self.advance();
9058 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9059 }
9060 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9061 name,
9062 table,
9063 rename_to: None,
9064 roles,
9065 using,
9066 with_check,
9067 }))
9068 }
9069
9070 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9071 /// `DROP POLICY`.
9072 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9073 let if_exists = self.consume_if_exists();
9074 let name = self.expect_ident_like()?;
9075 if !matches!(self.peek(), Token::On) {
9076 return Err(self.err(alloc::format!(
9077 "expected ON after DROP POLICY name, got {:?}",
9078 self.peek()
9079 )));
9080 }
9081 self.advance();
9082 let table = self.expect_ident_like()?;
9083 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9084 name,
9085 table,
9086 if_exists,
9087 }))
9088 }
9089}
9090fn wrap_from_leaves(
9091 e: &mut Expr,
9092 names: &[String],
9093 make: &dyn Fn(Expr) -> Expr,
9094 refs: &dyn Fn(&Expr) -> bool,
9095) {
9096 if let Expr::Column(c) = e {
9097 if c.qualifier
9098 .as_deref()
9099 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9100 {
9101 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9102 *e = make(taken);
9103 }
9104 return;
9105 }
9106 match e {
9107 Expr::Binary { lhs, rhs, .. } => {
9108 wrap_from_leaves(lhs, names, make, refs);
9109 wrap_from_leaves(rhs, names, make, refs);
9110 }
9111 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9112 wrap_from_leaves(expr, names, make, refs)
9113 }
9114 Expr::FunctionCall { args, .. } => {
9115 for a in args.iter_mut() {
9116 wrap_from_leaves(a, names, make, refs);
9117 }
9118 }
9119 Expr::Case {
9120 operand,
9121 branches,
9122 else_branch,
9123 } => {
9124 if let Some(o) = operand.as_deref_mut() {
9125 wrap_from_leaves(o, names, make, refs);
9126 }
9127 for (w, t) in branches.iter_mut() {
9128 wrap_from_leaves(w, names, make, refs);
9129 wrap_from_leaves(t, names, make, refs);
9130 }
9131 if let Some(el) = else_branch.as_deref_mut() {
9132 wrap_from_leaves(el, names, make, refs);
9133 }
9134 }
9135 // Compound variants the walk doesn't decompose: keep the
9136 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9137 // a source table, so nothing regresses.
9138 other => {
9139 if refs(other) {
9140 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9141 *other = make(taken);
9142 }
9143 }
9144 }
9145}
9146
9147/// v7.39 (round 241) — does this expression reference any of the FROM /
9148/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9149/// lowerings)?
9150fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9151 match e {
9152 Expr::Column(c) => c
9153 .qualifier
9154 .as_deref()
9155 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9156 Expr::Binary { lhs, rhs, .. } => {
9157 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9158 }
9159 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9160 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9161 Expr::Case {
9162 operand,
9163 branches,
9164 else_branch,
9165 } => {
9166 operand
9167 .as_deref()
9168 .is_some_and(|o| expr_refs_tables(o, names))
9169 || branches
9170 .iter()
9171 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9172 || else_branch
9173 .as_deref()
9174 .is_some_and(|el| expr_refs_tables(el, names))
9175 }
9176 _ => false,
9177 }
9178}
9179
9180impl Parser {
9181 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9182 /// Caller already consumed the leading `UPDATE` ident.
9183 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9184 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9185 /// after the target name has been read. `JOIN` is a reserved token;
9186 /// the qualifiers are bare idents.
9187 fn peek_is_update_join_start(&self) -> bool {
9188 match self.peek() {
9189 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9190 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9191 Token::Join
9192 | Token::Inner
9193 | Token::Left
9194 | Token::Right
9195 | Token::Cross
9196 | Token::Full => true,
9197 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9198 Token::Ident(s) | Token::QuotedIdent(s) => {
9199 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9200 }
9201 _ => false,
9202 }
9203 }
9204
9205 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9206 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9207 /// expression on the right, and `:=` as a second spelling of `=`.
9208 ///
9209 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9210 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9211 /// and holding this loop's `Vec` + `String` locals there overflowed the
9212 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9213 #[inline(never)]
9214 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9215 let mut assigns: Vec<(String, Expr)> = Vec::new();
9216 let mut settings: Vec<(String, Expr)> = Vec::new();
9217 loop {
9218 // v7.39 (round 554) — a plain NAME here is a session
9219 // setting, not a user variable. mysqldump writes the two in
9220 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9221 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9222 // changes it — and this refused the mixture outright, so no
9223 // dump could be restored past its preamble.
9224 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9225 self.advance();
9226 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9227 return Err(self.err(alloc::format!(
9228 "expected `=` after {name}, got {:?}",
9229 self.peek()
9230 )));
9231 }
9232 self.advance();
9233 let value = self.parse_expr(0)?;
9234 settings.push((name.to_ascii_lowercase(), value));
9235 if matches!(self.peek(), Token::Comma) {
9236 self.advance();
9237 continue;
9238 }
9239 break;
9240 }
9241 let Token::SessionVar(raw) = self.peek().clone() else {
9242 return Err(self.err(alloc::format!(
9243 "expected a user variable after SET, got {:?}",
9244 self.peek()
9245 )));
9246 };
9247 if raw.starts_with("@@") {
9248 return Err(self.err(alloc::string::String::from(
9249 "cannot mix `@@` settings with `@` user variables in one SET",
9250 )));
9251 }
9252 self.advance();
9253 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9254 return Err(self.err(alloc::format!(
9255 "expected `=` or `:=` after {raw}, got {:?}",
9256 self.peek()
9257 )));
9258 }
9259 self.advance();
9260 let value = self.parse_expr(0)?;
9261 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9262 if matches!(self.peek(), Token::Comma) {
9263 self.advance();
9264 continue;
9265 }
9266 break;
9267 }
9268 Ok(Statement::SetUserVars(assigns, settings))
9269 }
9270
9271 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9272 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9273 // NAMED `only` until now, which failed on `relation "only" does
9274 // not exist`. The lookahead is what keeps a table actually
9275 // called `only` working: the keyword is only a keyword when a
9276 // TABLE NAME follows it — and `SET` arrives as an identifier
9277 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9278 // for the table and die on the `=`. Measured by the pin.
9279 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9280 if s.eq_ignore_ascii_case("only"))
9281 && matches!(
9282 self.tokens.get(self.pos + 1),
9283 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9284 );
9285 if only {
9286 self.advance();
9287 }
9288 let table = self.expect_ident_like()?;
9289 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9290 // bare spelling; a bare identifier that is the SET keyword itself
9291 // is the clause, not an alias.
9292 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9293 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9294 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9295 // following JOIN a syntax error.
9296 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9297 let alias = if matches!(self.peek(), Token::As) {
9298 self.advance();
9299 Some(self.expect_ident_like()?)
9300 } else {
9301 match self.peek() {
9302 Token::Ident(s) | Token::QuotedIdent(s)
9303 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9304 {
9305 let a = s.clone();
9306 self.advance();
9307 Some(a)
9308 }
9309 _ => None,
9310 }
9311 };
9312 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9313 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9314 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9315 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9316 // The FIRST table is the mutation target and the rest are sources —
9317 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9318 // SPG already lowers onto correlated subqueries. So rewind, let
9319 // `parse_from_clause` read the whole list (it handles aliases, comma
9320 // lists, and every JOIN form), then peel the target off the front.
9321 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9322 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9323 {
9324 // NOTE: `advance()` destroys the tokens it returns
9325 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9326 // is NOT possible — the tail is read forward, once, through the
9327 // same grammar `parse_from_clause` uses after its primary.
9328 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9329 let mut joins = self.parse_from_joins(&target_qual)?;
9330 if joins.is_empty() {
9331 return Err(self.err(alloc::string::String::from(
9332 "multi-table UPDATE needs at least one source table",
9333 )));
9334 }
9335 let head = joins.remove(0);
9336 // A LEFT join keeps every target row (the unmatched ones see NULL
9337 // on the source side), so it must NOT get the EXISTS row filter
9338 // the inner / comma forms use.
9339 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9340 let src = FromClause {
9341 primary: head.table,
9342 joins,
9343 };
9344 (Some(src), head.on, outer)
9345 } else {
9346 (None, None, false)
9347 };
9348 self.expect_keyword_ident("set")?;
9349 let mut assignments = Vec::new();
9350 loop {
9351 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9352 // …)` — the parenthesized multi-assignment. Expressions
9353 // assign positionally; a subquery RHS clones per column
9354 // keeping only the Nth projection item.
9355 if matches!(self.peek(), Token::LParen) {
9356 self.advance();
9357 let mut cols = alloc::vec![self.expect_ident_like()?];
9358 while matches!(self.peek(), Token::Comma) {
9359 self.advance();
9360 cols.push(self.expect_ident_like()?);
9361 }
9362 if !matches!(self.peek(), Token::RParen) {
9363 return Err(self.err(format!(
9364 "expected ')' after SET column list, got {:?}",
9365 self.peek()
9366 )));
9367 }
9368 self.advance();
9369 if !matches!(self.peek(), Token::Eq) {
9370 return Err(self.err(format!(
9371 "expected `=` after SET column list, got {:?}",
9372 self.peek()
9373 )));
9374 }
9375 self.advance();
9376 if !matches!(self.peek(), Token::LParen) {
9377 return Err(self.err(format!(
9378 "expected '(' after SET (…) =, got {:?}",
9379 self.peek()
9380 )));
9381 }
9382 self.advance();
9383 if matches!(self.peek(), Token::Select) {
9384 let inner = match self.parse_select_stmt()? {
9385 Statement::Select(s) => s,
9386 other => {
9387 return Err(self.err(alloc::format!(
9388 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9389 )));
9390 }
9391 };
9392 if !matches!(self.peek(), Token::RParen) {
9393 return Err(self.err(format!(
9394 "expected ')' after SET subquery, got {:?}",
9395 self.peek()
9396 )));
9397 }
9398 self.advance();
9399 if inner.items.len() != cols.len() {
9400 return Err(self.err(alloc::format!(
9401 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9402 cols.len(),
9403 inner.items.len()
9404 )));
9405 }
9406 for (i, col) in cols.into_iter().enumerate() {
9407 let mut sub = inner.clone();
9408 sub.items = alloc::vec![sub.items[i].clone()];
9409 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9410 }
9411 } else {
9412 let mut exprs = alloc::vec![self.parse_expr(0)?];
9413 while matches!(self.peek(), Token::Comma) {
9414 self.advance();
9415 exprs.push(self.parse_expr(0)?);
9416 }
9417 if !matches!(self.peek(), Token::RParen) {
9418 return Err(self.err(format!(
9419 "expected ')' after SET row values, got {:?}",
9420 self.peek()
9421 )));
9422 }
9423 self.advance();
9424 if exprs.len() != cols.len() {
9425 return Err(self.err(alloc::format!(
9426 "SET (…) = (…) arity mismatch: {} columns, {} values",
9427 cols.len(),
9428 exprs.len()
9429 )));
9430 }
9431 for (col, e) in cols.into_iter().zip(exprs) {
9432 assignments.push((col, e));
9433 }
9434 }
9435 if matches!(self.peek(), Token::Comma) {
9436 self.advance();
9437 continue;
9438 }
9439 break;
9440 }
9441 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9442 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9443 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9444 // `public.` dump qualifiers), so the qualifier has to be read off
9445 // the token stream first — otherwise `SET b.v = 888` would write
9446 // to the TARGET table's `v` while naming a source table, a
9447 // silent-wrong. A qualifier naming a SOURCE table means a
9448 // multi-TARGET update — mutating two tables in one statement —
9449 // which SPG does not model, so it is refused loudly.
9450 let set_qual: Option<String> = if mysql_from.is_some()
9451 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9452 {
9453 match self.peek() {
9454 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9455 _ => None,
9456 }
9457 } else {
9458 None
9459 };
9460 let col = self.expect_ident_like()?;
9461 if let Some(q) = set_qual {
9462 let names_target = q.eq_ignore_ascii_case(&table)
9463 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9464 if !names_target {
9465 return Err(self.err(alloc::format!(
9466 "multi-table UPDATE can only assign to its first table \
9467 ({table}); `{q}.{col}` targets another table"
9468 )));
9469 }
9470 }
9471 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9472 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9473 // `__column_default` marker lowering just below). PG assigns to the
9474 // i-th (1-based) element, NULL-padding when i exceeds the length.
9475 if matches!(self.peek(), Token::LBracket) {
9476 self.advance();
9477 let index = self.parse_expr(0)?;
9478 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9479 // (and the open `arr[lo:]`), lowered to
9480 // `__array_assign_slice`. Only the single-subscript form
9481 // parsed before, so a slice assignment was a syntax error.
9482 let mut slice_hi: Option<Option<Expr>> = None;
9483 if matches!(self.peek(), Token::Colon) {
9484 self.advance();
9485 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9486 None
9487 } else {
9488 Some(self.parse_expr(0)?)
9489 });
9490 }
9491 if !matches!(self.peek(), Token::RBracket) {
9492 return Err(self.err(format!(
9493 "expected `]` after array subscript in UPDATE SET, got {:?}",
9494 self.peek()
9495 )));
9496 }
9497 self.advance();
9498 if !matches!(self.peek(), Token::Eq) {
9499 return Err(self.err(format!(
9500 "expected `=` after array subscript in UPDATE SET, got {:?}",
9501 self.peek()
9502 )));
9503 }
9504 self.advance();
9505 let value = self.parse_expr(0)?;
9506 // PG merges several subscript writes to the same column into one
9507 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9508 // assignment to `col` rather than each overwriting the original.
9509 let existing = assignments.iter().position(|(c, _)| c == &col);
9510 let base = match existing {
9511 Some(i) => assignments[i].1.clone(),
9512 None => Expr::Column(ColumnName {
9513 qualifier: None,
9514 name: col.clone(),
9515 }),
9516 };
9517 let assigned = match slice_hi {
9518 None => Expr::FunctionCall {
9519 name: "__array_assign".to_string(),
9520 args: alloc::vec![base, index, value],
9521 },
9522 Some(hi) => Expr::FunctionCall {
9523 name: "__array_assign_slice".to_string(),
9524 args: alloc::vec![
9525 base,
9526 index,
9527 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9528 value,
9529 ],
9530 },
9531 };
9532 match existing {
9533 Some(i) => assignments[i].1 = assigned,
9534 None => assignments.push((col, assigned)),
9535 }
9536 if matches!(self.peek(), Token::Comma) {
9537 self.advance();
9538 continue;
9539 }
9540 break;
9541 }
9542 if !matches!(self.peek(), Token::Eq) {
9543 return Err(self.err(format!(
9544 "expected `=` after column name in UPDATE SET, got {:?}",
9545 self.peek()
9546 )));
9547 }
9548 self.advance();
9549 // `SET col = DEFAULT` — the column's declared default;
9550 // rides out as a marker call the update executor
9551 // resolves against the schema.
9552 let value = if matches!(self.peek(), Token::Default) {
9553 self.advance();
9554 Expr::FunctionCall {
9555 name: "__column_default".to_string(),
9556 args: Vec::new(),
9557 }
9558 } else {
9559 self.parse_expr(0)?
9560 };
9561 assignments.push((col, value));
9562 if matches!(self.peek(), Token::Comma) {
9563 self.advance();
9564 continue;
9565 }
9566 break;
9567 }
9568 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9569 // update. Lowers onto the correlated-subquery machinery:
9570 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9571 // and each assignment that references a FROM-list table
9572 // wraps into a correlated scalar subquery
9573 // (SELECT expr FROM src WHERE cond). Equivalent for the
9574 // unique-join shape (the overwhelmingly common one); a
9575 // multi-match, which PG resolves by arbitrary pick,
9576 // surfaces as a scalar-subquery cardinality error instead
9577 // of a silent arbitrary result.
9578 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9579 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9580 // the SAME lowering below. Both spellings together is not legal in
9581 // either dialect.
9582 let from_clause = if let Some(fc) = mysql_from {
9583 if matches!(self.peek(), Token::From) {
9584 return Err(self.err(alloc::string::String::from(
9585 "multi-table UPDATE already names its sources; drop the FROM clause",
9586 )));
9587 }
9588 Some(fc)
9589 } else if matches!(self.peek(), Token::From) {
9590 self.advance();
9591 Some(self.parse_from_clause()?)
9592 } else {
9593 None
9594 };
9595 let where_ = if matches!(self.peek(), Token::Where) {
9596 self.advance();
9597 Some(self.parse_expr(0)?)
9598 } else {
9599 None
9600 };
9601 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9602 // and the TARGET-row filter are NOT the same predicate once a LEFT
9603 // join is involved:
9604 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9605 // one conjunction, and the whole thing filters target rows via
9606 // EXISTS.
9607 // * LEFT join: only the ON predicate belongs inside the source
9608 // subquery. The WHERE still filters TARGET rows (with source
9609 // columns read through the correlated subquery, which yields NULL
9610 // for an unmatched row — exactly LEFT-join semantics).
9611 // Round 420 folded ON into WHERE unconditionally and then dropped the
9612 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9613 // WHERE a.id > 1` updated EVERY row.
9614 let sub_where = match (mysql_on.clone(), where_.clone()) {
9615 _ if mysql_outer => mysql_on.clone(),
9616 (Some(on), Some(w)) => Some(Expr::Binary {
9617 lhs: Box::new(on),
9618 op: crate::ast::BinOp::And,
9619 rhs: Box::new(w),
9620 }),
9621 (Some(on), None) => Some(on),
9622 (None, w) => w,
9623 };
9624 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9625 // has no such clause on UPDATE, so this is accepted only under the
9626 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9627 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9628 let mut returning = self.parse_optional_returning()?;
9629 // v7.39 (round 533) — kept for the engine, which can resolve the
9630 // UNQUALIFIED leaves this lowering has to leave alone.
9631 let from_sources = from_clause.as_ref().map(|fc| {
9632 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9633 from: fc.clone(),
9634 sub_where: sub_where.clone(),
9635 })
9636 });
9637 let (assignments, where_) = if let Some(fc) = from_clause {
9638 let names: Vec<String> = core::iter::once(&fc.primary)
9639 .chain(fc.joins.iter().map(|j| &j.table))
9640 .flat_map(|t| {
9641 t.alias
9642 .clone()
9643 .into_iter()
9644 .chain(core::iter::once(t.name.clone()))
9645 })
9646 .collect();
9647 let refs_list = |e: &Expr| -> bool {
9648 fn walk(e: &Expr, names: &[String]) -> bool {
9649 match e {
9650 Expr::Column(c) => c
9651 .qualifier
9652 .as_deref()
9653 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9654 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9655 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9656 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9657 Expr::Case {
9658 operand,
9659 branches,
9660 else_branch,
9661 } => {
9662 operand.as_deref().is_some_and(|o| walk(o, names))
9663 || branches
9664 .iter()
9665 .any(|(w, t)| walk(w, names) || walk(t, names))
9666 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9667 }
9668 _ => false,
9669 }
9670 }
9671 walk(e, &names)
9672 };
9673 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9674 locking: None,
9675 ctes: Vec::new(),
9676 distinct: false,
9677 distinct_on: Vec::new(),
9678 items,
9679 from: Some(fc.clone()),
9680 where_: sub_where.clone(),
9681 group_by: None,
9682 group_by_all: false,
9683 having: None,
9684 unions: Vec::new(),
9685 order_by: Vec::new(),
9686 limit: None,
9687 offset: None,
9688 limit_with_ties: false,
9689 window_check_exprs: Vec::new(),
9690 };
9691 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9692 // assignment RHS with a correlated scalar subquery, instead of
9693 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9694 // column reference (`SET v = v + u.bonus`, where `v` is the target
9695 // table's column) inside a subquery whose FROM only has the source
9696 // table, so the unqualified `v` resolved against the source and
9697 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9698 // context — where they belong — fixes it; only the source columns
9699 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9700 // compound variants the leaf-walk doesn't decompose.
9701 let make_subq = |inner: Expr| {
9702 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9703 expr: inner,
9704 alias: None,
9705 }])))
9706 };
9707 let assignments = assignments
9708 .into_iter()
9709 .map(|(col, mut expr)| {
9710 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9711 (col, expr)
9712 })
9713 .collect();
9714 let exists = Expr::Exists {
9715 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9716 expr: Expr::Literal(Literal::Integer(1)),
9717 alias: None,
9718 }])),
9719 negated: false,
9720 };
9721 // v7.39 (round 241) — RETURNING may reference the FROM-list
9722 // tables too (`RETURNING emp.id, dept.name`); the same
9723 // leaf-to-correlated-subquery lowering the assignments get.
9724 // Without it the qualifier died at eval with "unknown table
9725 // qualifier". (RETURNING was parsed before this block — the
9726 // lowering is a pure AST transformation.)
9727 if let Some(items) = returning.as_mut() {
9728 for item in items.iter_mut() {
9729 if let SelectItem::Expr { expr, .. } = item {
9730 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9731 }
9732 }
9733 }
9734 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9735 // EVERY matching target row: it gets no EXISTS filter, but the
9736 // caller's WHERE still applies, with source columns read through
9737 // the correlated subquery (NULL when unmatched — LEFT-join
9738 // semantics). `sub_where` above already excluded the WHERE from
9739 // the source subquery for this case.
9740 if mysql_outer {
9741 let mut outer = where_;
9742 if let Some(w) = outer.as_mut() {
9743 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9744 }
9745 (assignments, outer)
9746 } else {
9747 (assignments, Some(exists))
9748 }
9749 } else {
9750 (assignments, where_)
9751 };
9752 Ok(Statement::Update(crate::ast::UpdateStatement {
9753 ctes: Vec::new(),
9754 table,
9755 only,
9756 alias,
9757 assignments,
9758 from_sources,
9759 where_,
9760 order_limit: update_order_limit,
9761 returning,
9762 }))
9763 }
9764
9765 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9766 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9767 /// clause and its meaning are identical, so both call this rather than
9768 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9769 /// legal. PG has no such clause on either statement, so it is read only
9770 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9771 /// errors.
9772 ///
9773 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9774 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9775 /// stack in round 430.
9776 #[inline(never)]
9777 fn parse_mysql_dml_order_limit(
9778 &mut self,
9779 what: &str,
9780 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9781 if !self.mysql_dialect {
9782 return Ok(None);
9783 }
9784 let order_by = self.parse_order_by_keys()?;
9785 let limit = if matches!(self.peek(), Token::Limit) {
9786 self.advance();
9787 let tok = self.advance();
9788 let Token::Integer(n) = tok else {
9789 return Err(self.err(alloc::format!(
9790 "expected integer after {what} LIMIT, got {tok:?}"
9791 )));
9792 };
9793 // MySQL rejects the `LIMIT offset, count` form here — only a
9794 // single row count is legal on a DML statement.
9795 if matches!(self.peek(), Token::Comma) {
9796 return Err(self.err(alloc::format!(
9797 "{what} LIMIT takes a row count, not an offset"
9798 )));
9799 }
9800 let n = u32::try_from(n)
9801 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9802 Some(n)
9803 } else {
9804 None
9805 };
9806 if order_by.is_empty() && limit.is_none() {
9807 return Ok(None);
9808 }
9809 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9810 order_by,
9811 limit,
9812 })))
9813 }
9814
9815 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9816 /// the leading `DELETE` ident.
9817 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9818 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9819 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9820 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9821 // parse here; it reaches the existing USING path with the target
9822 // repeated in the list, which the source-list peel below handles.)
9823 // More than one name is a multi-TARGET delete, which SPG does not
9824 // model; it is refused rather than half-applied.
9825 let mysql_pre_target: Option<String> =
9826 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9827 let first = self.expect_ident_like()?;
9828 if matches!(self.peek(), Token::Comma) {
9829 return Err(self.err(alloc::format!(
9830 "multi-table DELETE can only delete from one table; \
9831 `DELETE {first}, …` names several"
9832 )));
9833 }
9834 Some(first)
9835 } else {
9836 None
9837 };
9838 if !matches!(self.peek(), Token::From) {
9839 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9840 }
9841 self.advance();
9842 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9843 // lookahead as the UPDATE spelling.
9844 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9845 if s.eq_ignore_ascii_case("only"))
9846 && matches!(
9847 self.tokens.get(self.pos + 1),
9848 Some(Token::Ident(_) | Token::QuotedIdent(_))
9849 );
9850 if only {
9851 self.advance();
9852 }
9853 let table = self.expect_ident_like()?;
9854 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9855 // spelling must not swallow the clause keywords that can follow
9856 // the target.
9857 let alias = if matches!(self.peek(), Token::As) {
9858 self.advance();
9859 Some(self.expect_ident_like()?)
9860 } else {
9861 match self.peek() {
9862 Token::Ident(s) | Token::QuotedIdent(s)
9863 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9864 {
9865 let a = s.clone();
9866 self.advance();
9867 Some(a)
9868 }
9869 _ => None,
9870 }
9871 };
9872 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9873 // through the SAME join grammar the FROM clause uses (see the
9874 // `advance()`-destroys-tokens note on `parse_from_joins`).
9875 let mut mysql_on: Option<Expr> = None;
9876 let mut mysql_outer = false;
9877 let mysql_using = if mysql_pre_target.is_some()
9878 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9879 {
9880 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9881 let mut joins = self.parse_from_joins(&target_qual)?;
9882 if joins.is_empty() {
9883 return Err(self.err(alloc::string::String::from(
9884 "multi-table DELETE needs at least one source table",
9885 )));
9886 }
9887 let head = joins.remove(0);
9888 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9889 mysql_on = head.on;
9890 Some(FromClause {
9891 primary: head.table,
9892 joins,
9893 })
9894 } else {
9895 None
9896 };
9897 // The pre-FROM target must be the table the FROM names (or its
9898 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9899 // is not the scan target.
9900 if let Some(t) = &mysql_pre_target {
9901 let names_target = t.eq_ignore_ascii_case(&table)
9902 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9903 if !names_target {
9904 return Err(self.err(alloc::format!(
9905 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9906 )));
9907 }
9908 }
9909 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9910 // delete. Same lowering as UPDATE … FROM: the WHERE
9911 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9912 // target row by the correlated machinery.
9913 let using_clause = if let Some(fc) = mysql_using {
9914 Some(fc)
9915 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9916 self.advance();
9917 let mut fc = self.parse_from_clause()?;
9918 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9919 // repeats the TARGET as the first USING entry (PG's spelling
9920 // lists only the extra sources). Peel it so the source subquery
9921 // does not re-scan — and shadow — the target table.
9922 let primary_is_target =
9923 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9924 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9925 let head = fc.joins.remove(0);
9926 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9927 mysql_on = head.on;
9928 fc = FromClause {
9929 primary: head.table,
9930 joins: fc.joins,
9931 };
9932 }
9933 Some(fc)
9934 } else {
9935 None
9936 };
9937 let where_ = if matches!(self.peek(), Token::Where) {
9938 self.advance();
9939 Some(self.parse_expr(0)?)
9940 } else {
9941 None
9942 };
9943 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9944 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9945 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9946 let mut returning = self.parse_optional_returning()?;
9947 let where_ = if let Some(fc) = using_clause {
9948 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9949 // a USING-table reference in RETURNING becomes a correlated
9950 // scalar subquery over the USING list.
9951 let names: Vec<String> = core::iter::once(&fc.primary)
9952 .chain(fc.joins.iter().map(|j| &j.table))
9953 .flat_map(|t| {
9954 t.alias
9955 .clone()
9956 .into_iter()
9957 .chain(core::iter::once(t.name.clone()))
9958 })
9959 .collect();
9960 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9961 // join filters the SOURCE subquery on the ON predicate alone and
9962 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9963 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9964 // rows); every other form folds ON and WHERE into one EXISTS.
9965 let sub_where = match (mysql_on.clone(), where_.clone()) {
9966 _ if mysql_outer => mysql_on.clone(),
9967 (Some(on), Some(w)) => Some(Expr::Binary {
9968 lhs: Box::new(on),
9969 op: crate::ast::BinOp::And,
9970 rhs: Box::new(w),
9971 }),
9972 (Some(on), None) => Some(on),
9973 (None, w) => w,
9974 };
9975 let exists_where = sub_where.clone();
9976 let sub_fc = fc.clone();
9977 let make_subq = move |leaf: Expr| -> Expr {
9978 Expr::ScalarSubquery(Box::new(SelectStatement {
9979 locking: None,
9980 ctes: Vec::new(),
9981 distinct: false,
9982 distinct_on: Vec::new(),
9983 items: alloc::vec![SelectItem::Expr {
9984 expr: leaf,
9985 alias: None,
9986 }],
9987 from: Some(sub_fc.clone()),
9988 where_: sub_where.clone(),
9989 group_by: None,
9990 group_by_all: false,
9991 having: None,
9992 unions: Vec::new(),
9993 order_by: Vec::new(),
9994 limit: None,
9995 offset: None,
9996 limit_with_ties: false,
9997 window_check_exprs: Vec::new(),
9998 }))
9999 };
10000 let refs = |e: &Expr| expr_refs_tables(e, &names);
10001 if let Some(items) = returning.as_mut() {
10002 for item in items.iter_mut() {
10003 if let SelectItem::Expr { expr, .. } = item {
10004 wrap_from_leaves(expr, &names, &make_subq, &refs);
10005 }
10006 }
10007 }
10008 // A LEFT join deletes the target rows the WHERE selects, reading
10009 // source columns through the correlated subquery (NULL when
10010 // unmatched); no EXISTS row filter.
10011 if mysql_outer {
10012 let mut outer = where_;
10013 if let Some(w) = outer.as_mut() {
10014 wrap_from_leaves(w, &names, &make_subq, &refs);
10015 }
10016 outer
10017 } else {
10018 Some(Expr::Exists {
10019 subquery: Box::new(SelectStatement {
10020 locking: None,
10021 ctes: Vec::new(),
10022 distinct: false,
10023 distinct_on: Vec::new(),
10024 items: alloc::vec![SelectItem::Expr {
10025 expr: Expr::Literal(Literal::Integer(1)),
10026 alias: None,
10027 }],
10028 from: Some(fc),
10029 where_: exists_where,
10030 group_by: None,
10031 group_by_all: false,
10032 having: None,
10033 unions: Vec::new(),
10034 order_by: Vec::new(),
10035 limit: None,
10036 offset: None,
10037 limit_with_ties: false,
10038 window_check_exprs: Vec::new(),
10039 }),
10040 negated: false,
10041 })
10042 }
10043 } else {
10044 where_
10045 };
10046 Ok(Statement::Delete(crate::ast::DeleteStatement {
10047 ctes: Vec::new(),
10048 table,
10049 only,
10050 alias,
10051 where_,
10052 order_limit: delete_order_limit,
10053 returning,
10054 }))
10055 }
10056
10057 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10058 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10059 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10060 /// keyword. v7.17 surface:
10061 /// * source: table reference (subquery source is a follow-up)
10062 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10063 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10064 /// * AND-conditioned WHEN clauses; clauses tried in declaration
10065 /// order
10066 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10067 // INTO
10068 let is_into_kw = matches!(self.peek(), Token::Into)
10069 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10070 if !is_into_kw {
10071 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10072 }
10073 self.advance();
10074 let target = self.expect_ident_like()?;
10075 // Optional alias — bare ident before USING.
10076 let target_alias = match self.peek() {
10077 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10078 Some(self.expect_ident_like()?)
10079 }
10080 _ => None,
10081 };
10082 // USING
10083 let is_using_kw = matches!(
10084 self.peek(),
10085 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10086 );
10087 if !is_using_kw {
10088 return Err(self.err(format!(
10089 "expected USING after MERGE INTO target, got {:?}",
10090 self.peek()
10091 )));
10092 }
10093 self.advance();
10094 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10095 // <table> [alias]`. PG requires an alias after a subquery source.
10096 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10097 self.advance(); // (
10098 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10099 // constant-SELECT lowering the derived-table parser uses
10100 // (PG deletes through this form; it was a parse error).
10101 let inner = if matches!(self.peek(), Token::Values) {
10102 self.advance(); // VALUES
10103 Statement::Select(self.parse_values_rows_body()?)
10104 } else {
10105 self.parse_select_stmt()?
10106 };
10107 match self.advance() {
10108 Token::RParen => {}
10109 other => {
10110 return Err(self.err(format!(
10111 "expected ')' after MERGE USING subquery, got {other:?}"
10112 )));
10113 }
10114 }
10115 let Statement::Select(sub) = inner else {
10116 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10117 };
10118 (String::new(), Some(Box::new(sub)))
10119 } else {
10120 (self.expect_ident_like()?, None)
10121 };
10122 let source_alias = match self.peek() {
10123 Token::Ident(s) | Token::QuotedIdent(s)
10124 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10125 {
10126 Some(self.expect_ident_like()?)
10127 }
10128 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10129 self.advance(); // AS
10130 Some(self.expect_ident_like()?)
10131 }
10132 _ => None,
10133 };
10134 // v7.39 (round 768, F31-D5) — optional positional column-alias
10135 // list after the source alias (`s(id, v)`).
10136 let mut source_column_aliases: Vec<String> = Vec::new();
10137 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10138 self.advance();
10139 loop {
10140 source_column_aliases.push(self.expect_ident_like()?);
10141 match self.peek() {
10142 Token::Comma => {
10143 self.advance();
10144 }
10145 Token::RParen => {
10146 self.advance();
10147 break;
10148 }
10149 other => {
10150 return Err(self.err(format!(
10151 "expected ',' or ')' in MERGE source column list, got {other:?}"
10152 )));
10153 }
10154 }
10155 }
10156 }
10157 if source_select.is_some() && source_alias.is_none() {
10158 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10159 }
10160 // ON
10161 if !matches!(self.peek(), Token::On) {
10162 return Err(self.err(format!(
10163 "expected ON after MERGE … USING source, got {:?}",
10164 self.peek()
10165 )));
10166 }
10167 self.advance();
10168 let on = self.parse_expr(0)?;
10169 // One or more WHEN clauses.
10170 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10171 loop {
10172 let is_when_kw = matches!(
10173 self.peek(),
10174 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10175 );
10176 if !is_when_kw {
10177 break;
10178 }
10179 self.advance(); // WHEN
10180 // [NOT] MATCHED
10181 let matched = if matches!(self.peek(), Token::Not) {
10182 self.advance();
10183 crate::ast::MergeMatched::NotMatched
10184 } else {
10185 crate::ast::MergeMatched::Matched
10186 };
10187 let is_matched_kw = matches!(
10188 self.peek(),
10189 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10190 );
10191 if !is_matched_kw {
10192 return Err(self.err(format!(
10193 "expected MATCHED in WHEN clause, got {:?}",
10194 self.peek()
10195 )));
10196 }
10197 self.advance();
10198 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10199 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10200 // to fire for target rows no source row matches.
10201 let mut matched = matched;
10202 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10203 self.advance();
10204 match self.peek() {
10205 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10206 self.advance();
10207 matched = crate::ast::MergeMatched::NotMatchedBySource;
10208 }
10209 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10210 self.advance();
10211 }
10212 other => {
10213 return Err(self.err(format!(
10214 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10215 )));
10216 }
10217 }
10218 }
10219 // Optional AND <expr>
10220 let condition = if matches!(self.peek(), Token::And) {
10221 self.advance();
10222 Some(self.parse_expr(0)?)
10223 } else {
10224 None
10225 };
10226 // THEN
10227 let is_then_kw = matches!(
10228 self.peek(),
10229 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10230 );
10231 if !is_then_kw {
10232 return Err(self.err(format!(
10233 "expected THEN in WHEN clause, got {:?}",
10234 self.peek()
10235 )));
10236 }
10237 self.advance();
10238 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10239 let action = match self.peek().clone() {
10240 Token::Insert => {
10241 self.advance();
10242 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10243 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10244 // VALUES (…)` omits it and fills every column in declaration
10245 // order. PG accepts this; SPG used to require the list.
10246 let mut columns: Vec<String> = Vec::new();
10247 if matches!(self.peek(), Token::LParen) {
10248 self.advance();
10249 loop {
10250 columns.push(self.expect_ident_like()?);
10251 if matches!(self.peek(), Token::Comma) {
10252 self.advance();
10253 continue;
10254 }
10255 break;
10256 }
10257 if !matches!(self.peek(), Token::RParen) {
10258 return Err(self.err(format!(
10259 "expected ')' after INSERT column list, got {:?}",
10260 self.peek()
10261 )));
10262 }
10263 self.advance();
10264 }
10265 // VALUES (...)
10266 if !matches!(self.peek(), Token::Values) {
10267 return Err(self.err(format!(
10268 "expected VALUES in MERGE INSERT, got {:?}",
10269 self.peek()
10270 )));
10271 }
10272 self.advance();
10273 if !matches!(self.peek(), Token::LParen) {
10274 return Err(self.err(format!(
10275 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10276 self.peek()
10277 )));
10278 }
10279 self.advance();
10280 let mut values: Vec<crate::ast::Expr> = Vec::new();
10281 loop {
10282 values.push(self.parse_expr(0)?);
10283 if matches!(self.peek(), Token::Comma) {
10284 self.advance();
10285 continue;
10286 }
10287 break;
10288 }
10289 if !matches!(self.peek(), Token::RParen) {
10290 return Err(self.err(format!(
10291 "expected ')' after MERGE INSERT values, got {:?}",
10292 self.peek()
10293 )));
10294 }
10295 self.advance();
10296 // Empty column list = positional into every column, so the
10297 // count is checked against the table arity at execution.
10298 if !columns.is_empty() && columns.len() != values.len() {
10299 return Err(self.err(format!(
10300 "MERGE INSERT column count ({}) ≠ value count ({})",
10301 columns.len(),
10302 values.len()
10303 )));
10304 }
10305 crate::ast::MergeAction::Insert { columns, values }
10306 }
10307 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10308 self.advance();
10309 // SET
10310 let is_set_kw = matches!(
10311 self.peek(),
10312 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10313 );
10314 if !is_set_kw {
10315 return Err(self.err(format!(
10316 "expected SET after UPDATE in MERGE, got {:?}",
10317 self.peek()
10318 )));
10319 }
10320 self.advance();
10321 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10322 loop {
10323 let col = self.expect_ident_like()?;
10324 if !matches!(self.peek(), Token::Eq) {
10325 return Err(self.err(format!(
10326 "expected '=' in MERGE UPDATE assignment, got {:?}",
10327 self.peek()
10328 )));
10329 }
10330 self.advance();
10331 let expr = self.parse_expr(0)?;
10332 assignments.push((col, expr));
10333 if matches!(self.peek(), Token::Comma) {
10334 self.advance();
10335 continue;
10336 }
10337 break;
10338 }
10339 crate::ast::MergeAction::Update { assignments }
10340 }
10341 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10342 self.advance();
10343 crate::ast::MergeAction::Delete
10344 }
10345 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10346 self.advance();
10347 let is_nothing_kw = matches!(
10348 self.peek(),
10349 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10350 );
10351 if !is_nothing_kw {
10352 return Err(self.err(format!(
10353 "expected NOTHING after DO in MERGE clause, got {:?}",
10354 self.peek()
10355 )));
10356 }
10357 self.advance();
10358 crate::ast::MergeAction::DoNothing
10359 }
10360 other => {
10361 return Err(self.err(format!(
10362 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10363 )));
10364 }
10365 };
10366 // PG's grammar simply has no INSERT production under BY SOURCE
10367 // (a target row already exists there) — same syntax error.
10368 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10369 && matches!(action, crate::ast::MergeAction::Insert { .. })
10370 {
10371 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10372 }
10373 clauses.push(crate::ast::MergeWhenClause {
10374 matched,
10375 condition,
10376 action,
10377 });
10378 }
10379 if clauses.is_empty() {
10380 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10381 }
10382 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10383 // unconditional (no `AND`) WHEN of the same match kind: it could
10384 // never fire. Check per match kind in clause order.
10385 let mut seen_unconditional_matched = false;
10386 let mut seen_unconditional_not_matched = false;
10387 let mut seen_unconditional_by_source = false;
10388 for c in &clauses {
10389 let seen = match c.matched {
10390 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10391 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10392 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10393 };
10394 if *seen {
10395 return Err(self.err(String::from(
10396 "unreachable WHEN clause specified after unconditional WHEN clause",
10397 )));
10398 }
10399 if c.condition.is_none() {
10400 *seen = true;
10401 }
10402 }
10403 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10404 let returning = self.parse_optional_returning()?;
10405 Ok(Statement::Merge(crate::ast::MergeStatement {
10406 // Attached by `parse_with_cte_then_select` when the MERGE
10407 // heads a WITH clause (round 149).
10408 ctes: Vec::new(),
10409 target,
10410 target_alias,
10411 source,
10412 source_alias,
10413 source_select,
10414 source_column_aliases,
10415 on,
10416 clauses,
10417 returning,
10418 }))
10419 }
10420
10421 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10422 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10423 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10424 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10425 fn parse_optional_returning(
10426 &mut self,
10427 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10428 let is_returning_kw = matches!(
10429 self.peek(),
10430 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10431 );
10432 if !is_returning_kw {
10433 return Ok(None);
10434 }
10435 self.advance();
10436 let mut items = Vec::new();
10437 loop {
10438 items.push(self.parse_select_item()?);
10439 if matches!(self.peek(), Token::Comma) {
10440 self.advance();
10441 continue;
10442 }
10443 break;
10444 }
10445 Ok(Some(items))
10446 }
10447
10448 /// v6.0.4 — parse the tail of an ALTER statement after the
10449 /// leading `ALTER` keyword has been consumed. Only one form is
10450 /// supported in v6.0.4:
10451 ///
10452 /// ```text
10453 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10454 /// ```
10455 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10456 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10457 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10458 // exclusion) is accepted by stripping the `ONLY` keyword
10459 // before the table parse.
10460 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10461 // and the long PG-dump tail are accepted as no-ops.
10462 match self.advance() {
10463 Token::Index => {}
10464 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10465 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10466 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10467 Token::Table => {
10468 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10469 self.advance();
10470 }
10471 return self.parse_alter_table_after_keyword();
10472 }
10473 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10474 return self.parse_alter_policy_after_keyword();
10475 }
10476 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10477 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10478 self.advance();
10479 }
10480 return self.parse_alter_table_after_keyword();
10481 }
10482 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10483 // of the silent-noop tail.
10484 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10485 return self.parse_alter_sequence_after_keyword();
10486 }
10487 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10488 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10489 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10490 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10491 // NB: the match arm consumed `TYPE` via self.advance(); the
10492 // cursor is now at the type name — do NOT advance again.
10493 let type_name = self.expect_ident_like()?;
10494 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10495 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10496 if is_add_value {
10497 self.advance(); // ADD
10498 self.advance(); // VALUE
10499 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10500 // IF/EXISTS as identifiers.
10501 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10502 {
10503 let n1 = self.tokens.get(self.pos + 1);
10504 let n2 = self.tokens.get(self.pos + 2);
10505 if matches!(n1, Some(Token::Not))
10506 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10507 {
10508 self.advance();
10509 self.advance();
10510 self.advance();
10511 true
10512 } else {
10513 false
10514 }
10515 } else {
10516 false
10517 };
10518 let label = self.expect_string_literal()?;
10519 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10520 {
10521 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10522 self.advance();
10523 let anchor = self.expect_string_literal()?;
10524 Some((is_before, anchor))
10525 } else {
10526 None
10527 };
10528 return Ok(Statement::AlterTypeAddValue {
10529 type_name,
10530 label,
10531 if_not_exists,
10532 position,
10533 });
10534 }
10535 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10536 // Used to fall into the no-op tail below: accepted, silently
10537 // ignored. `RENAME TO <newtype>` keeps falling through.
10538 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10539 && matches!(
10540 self.tokens.get(self.pos + 1),
10541 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10542 )
10543 {
10544 self.advance(); // RENAME
10545 self.advance(); // VALUE
10546 let old = self.expect_string_literal()?;
10547 if matches!(self.peek(), Token::To) {
10548 self.advance();
10549 } else {
10550 self.expect_keyword_ident("to")?;
10551 }
10552 let new = self.expect_string_literal()?;
10553 return Ok(Statement::AlterTypeRenameValue {
10554 type_name,
10555 old,
10556 new,
10557 });
10558 }
10559 // Other ALTER TYPE forms — the ACTION stays a no-op
10560 // (pg_dump tail), but v7.39 (round 708) the NAME is
10561 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10562 // success for a type that does not exist.
10563 self.consume_until_statement_boundary();
10564 return Ok(Statement::ValidateOnly {
10565 kind: crate::ast::ValidateOnlyKind::TypeName,
10566 names: alloc::vec![type_name],
10567 });
10568 }
10569 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10570 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10571 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10572 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10573 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10574 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10575 // pg_dump no-op list below: every form used to report success
10576 // and change nothing, which is worse than refusing outright
10577 // (a migration dropping a constraint kept rejecting data).
10578 // NOTE: the enclosing `match self.advance()` already consumed
10579 // the DOMAIN keyword, so the name is next.
10580 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10581 return self.parse_alter_domain_after_keyword();
10582 }
10583 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10584 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10585 // used to fall into the pg_dump no-op tail below, so a DBA
10586 // setting a per-role default was told it worked and nothing
10587 // happened. Intercepted here, BEFORE that tail.
10588 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10589 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10590 // interception below exists: swallowed with the no-op tail, an
10591 // unknown parameter name was ACCEPTED where PG18 answers
10592 // `unrecognized configuration parameter`. SPG applies nothing
10593 // either way — there is no postgresql.auto.conf — but it now
10594 // says so about a name it does not know.
10595 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10596 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10597 // already consumed here. An extra advance eats the SET and
10598 // the parameter name is never seen — which is exactly the
10599 // bug a panic in this branch disproved: the branch WAS on
10600 // the path, the reading of it was wrong.
10601 let mut parameter = None;
10602 // SET <name> … | RESET <name> | RESET ALL
10603 if matches!(self.peek(), Token::Ident(k)
10604 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10605 {
10606 self.advance();
10607 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10608 && !n.eq_ignore_ascii_case("all")
10609 {
10610 self.advance();
10611 // A dotted GUC (`plpgsql.check_asserts`) is two
10612 // tokens; keep the whole name.
10613 let mut full = n;
10614 while matches!(self.peek(), Token::Dot) {
10615 self.advance();
10616 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10617 full.push('.');
10618 full.push_str(&t);
10619 }
10620 }
10621 parameter = Some(full);
10622 }
10623 }
10624 self.consume_until_statement_boundary();
10625 return Ok(Statement::AlterSystem { parameter });
10626 }
10627 Token::Ident(s) | Token::QuotedIdent(s)
10628 if matches!(
10629 s.to_ascii_lowercase().as_str(),
10630 "role" | "user" | "database"
10631 ) && self.peeks_db_role_setting() =>
10632 {
10633 let is_database = s.eq_ignore_ascii_case("database");
10634 return self.parse_db_role_setting(is_database);
10635 }
10636 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10637 // (the non-SET forms; SET/RESET took the branch above). The
10638 // attributes still no-op — recorded, and the ignored PASSWORD
10639 // is ledgered as its own follow-up — but the ROLE is validated:
10640 // any name was accepted for a role that does not exist.
10641 Token::Ident(s) | Token::QuotedIdent(s)
10642 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10643 {
10644 // NB: the enclosing `match self.advance()` already consumed
10645 // ROLE/USER — the round-695 trap, hit again in this round's
10646 // first draft (the name was eaten and WITH parsed as the
10647 // role). The cursor is at the name.
10648 let name = self.expect_ident_or_string()?;
10649 // v7.39 (round 750) — scan the attribute tail for
10650 // PASSWORD. Everything else stays a recorded no-op, but
10651 // a dropped credential rotation is a SECURITY bug:
10652 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10653 // changed nothing, so the old password kept working.
10654 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10655 // NULL` clears the credential.
10656 let mut password: Option<Option<String>> = None;
10657 loop {
10658 match self.peek() {
10659 Token::Semicolon | Token::Eof => break,
10660 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10661 self.advance();
10662 match self.advance() {
10663 Token::String(p) => password = Some(Some(p)),
10664 Token::Null => password = Some(None),
10665 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10666 password = Some(None);
10667 }
10668 other => {
10669 return Err(self.err(alloc::format!(
10670 "expected password string or NULL after PASSWORD, got {other:?}"
10671 )));
10672 }
10673 }
10674 }
10675 _ => {
10676 self.advance();
10677 }
10678 }
10679 }
10680 if name.eq_ignore_ascii_case("all") {
10681 // `ALTER ROLE ALL …` names every role; nothing to check.
10682 return Ok(Statement::Empty);
10683 }
10684 if let Some(pw) = password {
10685 return Ok(Statement::AlterRolePassword { name, password: pw });
10686 }
10687 return Ok(Statement::ValidateOnly {
10688 kind: crate::ast::ValidateOnlyKind::RoleName,
10689 names: alloc::vec![name],
10690 });
10691 }
10692 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10693 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10694 // list far enough to validate the NAME; the actions still no-op.
10695 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10696 // models none of them and their dumps are rare.)
10697 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10698 let name = self.expect_ident_or_string()?;
10699 self.consume_until_statement_boundary();
10700 return Ok(Statement::ValidateOnly {
10701 kind: crate::ast::ValidateOnlyKind::CollationName,
10702 names: alloc::vec![name],
10703 });
10704 }
10705 Token::Ident(s) | Token::QuotedIdent(s)
10706 if s.eq_ignore_ascii_case("text")
10707 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10708 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10709 {
10710 self.advance(); // SEARCH
10711 self.advance(); // CONFIGURATION
10712 let name = self.expect_ident_like()?;
10713 self.consume_until_statement_boundary();
10714 return Ok(Statement::ValidateOnly {
10715 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10716 names: alloc::vec![name],
10717 });
10718 }
10719 Token::Ident(s) | Token::QuotedIdent(s)
10720 if s.eq_ignore_ascii_case("event")
10721 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10722 {
10723 self.advance(); // TRIGGER
10724 let name = self.expect_ident_like()?;
10725 self.consume_until_statement_boundary();
10726 return Ok(Statement::ValidateOnly {
10727 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10728 names: alloc::vec![name],
10729 });
10730 }
10731 Token::Ident(s) | Token::QuotedIdent(s)
10732 if s.eq_ignore_ascii_case("large")
10733 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10734 {
10735 self.advance(); // OBJECT
10736 let oid = match self.advance() {
10737 Token::Integer(n) => alloc::format!("{n}"),
10738 other => {
10739 return Err(
10740 self.err(alloc::format!("expected large object oid, got {other:?}"))
10741 );
10742 }
10743 };
10744 self.consume_until_statement_boundary();
10745 return Ok(Statement::ValidateOnly {
10746 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10747 names: alloc::vec![oid],
10748 });
10749 }
10750 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10751 // argument-list parse as DROP AGGREGATE (round 707); the
10752 // action no-ops, the existence check is real.
10753 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10754 // Same round-695 trap as above: AGGREGATE is already
10755 // consumed; the cursor is at the name.
10756 let name = self.expect_ident_like()?;
10757 let mut names = alloc::vec![name];
10758 if matches!(self.peek(), Token::LParen) {
10759 self.advance();
10760 loop {
10761 match self.peek().clone() {
10762 Token::RParen => {
10763 self.advance();
10764 break;
10765 }
10766 Token::Star => {
10767 self.advance();
10768 names.push(String::from("*"));
10769 }
10770 Token::Comma => {
10771 self.advance();
10772 }
10773 _ => {
10774 let mut t = self.expect_ident_like()?;
10775 while let Token::Ident(nx) = self.peek() {
10776 let nx = nx.clone();
10777 self.advance();
10778 t.push(' ');
10779 t.push_str(&nx);
10780 }
10781 names.push(t);
10782 }
10783 }
10784 }
10785 }
10786 self.consume_until_statement_boundary();
10787 return Ok(Statement::ValidateOnly {
10788 kind: crate::ast::ValidateOnlyKind::AggregateName,
10789 names,
10790 });
10791 }
10792 Token::Ident(s) | Token::QuotedIdent(s)
10793 if matches!(
10794 s.to_ascii_lowercase().as_str(),
10795 "view"
10796 | "function"
10797 | "database"
10798 | "schema"
10799 | "owner"
10800 | "default"
10801 | "extension"
10802 | "materialized"
10803 | "publication"
10804 | "subscription"
10805 // v7.37.17 (17.6 siblings) — additional ALTER
10806 // targets pg_dump / pg_dumpall / operator DB
10807 // migration scripts commonly emit. SPG has
10808 // no matching machinery for any of these; the
10809 // parser accepts + Empty-returns so pg_dump
10810 // tail statements don't stall.
10811 | "tablespace"
10812 | "language"
10813 | "operator"
10814 | "conversion"
10815 | "statistics"
10816 | "server"
10817 | "foreign"
10818 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10819 // / TEMPLATE (CONFIGURATION intercepted above).
10820 | "text"
10821 ) =>
10822 {
10823 self.consume_until_statement_boundary();
10824 return Ok(Statement::Empty);
10825 }
10826 other => {
10827 return Err(self.err(format!(
10828 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10829 after ALTER, got {other:?}"
10830 )));
10831 }
10832 }
10833 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10834 // (mailrs migrate-042 ships these). The presence of an
10835 // IF EXISTS makes the subsequent name lookup tolerate
10836 // a missing index — engine returns CommandOk no-op.
10837 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10838 let next = self.tokens.get(self.pos + 1);
10839 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10840 self.advance();
10841 self.advance();
10842 true
10843 } else {
10844 false
10845 }
10846 } else {
10847 false
10848 };
10849 let name = self.expect_ident_like()?;
10850 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10851 // Detect BEFORE the REBUILD path so the existing REBUILD
10852 // arm stays untouched.
10853 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10854 self.advance();
10855 if matches!(self.peek(), Token::To) {
10856 self.advance();
10857 } else {
10858 self.expect_keyword_ident("to")?;
10859 }
10860 let new = self.expect_ident_like()?;
10861 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10862 name,
10863 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10864 }));
10865 }
10866 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10867 // A syntax error before; the index is validated, the params no-op.
10868 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10869 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10870 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10871 {
10872 self.consume_until_statement_boundary();
10873 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10874 name,
10875 target: crate::ast::AlterIndexTarget::StorageParams,
10876 }));
10877 }
10878 // REBUILD
10879 self.expect_keyword_ident("rebuild")?;
10880 // Optional: WITH (encoding = <enc>)
10881 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10882 self.advance();
10883 if !matches!(self.peek(), Token::LParen) {
10884 return Err(self.err(format!(
10885 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10886 self.peek()
10887 )));
10888 }
10889 self.advance();
10890 self.expect_keyword_ident("encoding")?;
10891 if !matches!(self.peek(), Token::Eq) {
10892 return Err(self.err(format!(
10893 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10894 self.peek()
10895 )));
10896 }
10897 self.advance();
10898 let enc_ident = match self.advance() {
10899 Token::Ident(s) | Token::QuotedIdent(s) => s,
10900 other => {
10901 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10902 }
10903 };
10904 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10905 "f32" => VecEncoding::F32,
10906 "sq8" => VecEncoding::Sq8,
10907 "half" => VecEncoding::F16,
10908 other => {
10909 return Err(self.err(format!(
10910 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10911 )));
10912 }
10913 };
10914 if !matches!(self.peek(), Token::RParen) {
10915 return Err(self.err(format!(
10916 "expected ')' after encoding value, got {:?}",
10917 self.peek()
10918 )));
10919 }
10920 self.advance();
10921 Some(enc)
10922 } else {
10923 None
10924 };
10925 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10926 name,
10927 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10928 }))
10929 }
10930
10931 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10932 /// only `SET` form currently supported; future v6.7.x can add
10933 /// more SET subjects without changing the dispatch shape.
10934 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10935 /// subactions. Single-subaction shape stays a 1-element vec.
10936 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10937 let table_name = self.expect_ident_like()?;
10938 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10939 loop {
10940 let subaction = self.parse_alter_table_subaction()?;
10941 // ADD COLUMN with inline REFERENCES emits both an
10942 // AddColumn and an AddForeignKey subaction; the
10943 // helper returns 1 or 2 items.
10944 targets.extend(subaction);
10945 if matches!(self.peek(), Token::Comma) {
10946 self.advance();
10947 continue;
10948 }
10949 break;
10950 }
10951 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10952 name: table_name,
10953 targets,
10954 }))
10955 }
10956
10957 /// Parse one ALTER TABLE subaction. Returns a Vec because
10958 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10959 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10960 /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
10961 /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
10962 /// appends.
10963 fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
10964 match self.peek() {
10965 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
10966 self.advance();
10967 Some(crate::ast::ColumnPosition::First)
10968 }
10969 Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
10970 self.advance();
10971 let name = self.expect_ident_like().ok()?;
10972 Some(crate::ast::ColumnPosition::After(name))
10973 }
10974 _ => None,
10975 }
10976 }
10977
10978 fn parse_alter_table_subaction(
10979 &mut self,
10980 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10981 match self.peek() {
10982 // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
10983 // a statement a real migration emits and SPG answered 1064
10984 // for; measured against MySQL 9.7.2, one at a time, beside
10985 // the published image.
10986 Token::Ident(s)
10987 if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
10988 {
10989 let changing = s.eq_ignore_ascii_case("change");
10990 self.advance();
10991 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
10992 self.advance();
10993 }
10994 // `parse_column_def_with_fk` reads the NAME itself, so
10995 // `MODIFY` hands it the column and `CHANGE` eats the old
10996 // name first and lets it read the new one.
10997 let old_name = if changing {
10998 Some(self.expect_ident_like()?)
10999 } else {
11000 None
11001 };
11002 let (definition, _fk) = self.parse_column_def_with_fk()?;
11003 let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11004 let rename_to = if changing {
11005 Some(definition.name.clone())
11006 } else {
11007 None
11008 };
11009 let position = self.parse_column_position();
11010 Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11011 column,
11012 rename_to,
11013 definition,
11014 position,
11015 }])
11016 }
11017 Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11018 self.advance();
11019 if matches!(self.peek(), Token::Eq) {
11020 self.advance();
11021 }
11022 let n = self.expect_u64_literal()?;
11023 Ok(alloc::vec![
11024 crate::ast::AlterTableTarget::SetTableAutoIncrement(
11025 i64::try_from(n).unwrap_or(i64::MAX)
11026 )
11027 ])
11028 }
11029 Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11030 self.advance();
11031 if matches!(self.peek(), Token::Eq) {
11032 self.advance();
11033 }
11034 // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11035 // clause has kept it since v7.39.3. The lexer folds a
11036 // bare identifier, and MySQL names the engine back
11037 // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11038 // answers `Unknown storage engine 'NoSuchEng'` there and
11039 // answered `'nosucheng'` here — the one thing that
11040 // message is for is telling the operator which word in
11041 // their migration was wrong.
11042 let at = self.pos;
11043 let name = self.expect_ident_like()?;
11044 let written = self
11045 .source_span(at, at)
11046 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11047 .filter(|raw| raw.eq_ignore_ascii_case(&name))
11048 .map(alloc::string::String::from);
11049 Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11050 written.unwrap_or(name)
11051 )])
11052 }
11053 Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11054 self.advance();
11055 // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11056 if matches!(self.peek(), Token::To) {
11057 self.advance();
11058 }
11059 let kw = self.expect_ident_like()?;
11060 if !kw.eq_ignore_ascii_case("character") {
11061 return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11062 }
11063 let set_kw = self.expect_ident_like()?;
11064 if !set_kw.eq_ignore_ascii_case("set") {
11065 return Err(self.err("expected SET after CHARACTER".into()));
11066 }
11067 let charset = self.expect_ident_like()?;
11068 let collate =
11069 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11070 self.advance();
11071 Some(self.expect_ident_like()?)
11072 } else {
11073 None
11074 };
11075 Ok(alloc::vec![
11076 crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11077 ])
11078 }
11079 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11080 self.advance();
11081 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11082 // storage parameters: paren-prefixed; consume.
11083 if matches!(self.peek(), Token::LParen) {
11084 self.consume_until_statement_boundary();
11085 return Ok(Vec::new());
11086 }
11087 let setting = self.expect_ident_like()?;
11088 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11089 if !matches!(self.peek(), Token::Eq) {
11090 return Err(self.err(alloc::format!(
11091 "expected '=' after hot_tier_bytes, got {:?}",
11092 self.peek()
11093 )));
11094 }
11095 self.advance();
11096 let n = self.expect_u64_literal()?;
11097 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11098 }
11099 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11100 // accept-and-no-op for ALTER TABLE SET <subject>
11101 // forms that pg_dump emits but SPG either treats
11102 // as N/A (single-tenant, single-owner, no shared
11103 // tablespaces) or accepts the dump-side declaration
11104 // without runtime effect:
11105 // SET SCHEMA <name> (18.11)
11106 // SET TABLESPACE <name> (18.8)
11107 // SET LOGGED / UNLOGGED (18.7 alt-form)
11108 // SET WITHOUT CLUSTER (18.13)
11109 // SET WITHOUT OIDS (PG legacy)
11110 // SET (option = value, …) (storage parameters)
11111 // SET REPLICA IDENTITY {…} (18.14)
11112 if setting.eq_ignore_ascii_case("schema")
11113 || setting.eq_ignore_ascii_case("tablespace")
11114 || setting.eq_ignore_ascii_case("logged")
11115 || setting.eq_ignore_ascii_case("unlogged")
11116 || setting.eq_ignore_ascii_case("without")
11117 {
11118 self.consume_until_statement_boundary();
11119 return Ok(Vec::new());
11120 }
11121 if setting.eq_ignore_ascii_case("replica") {
11122 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11123 self.consume_until_statement_boundary();
11124 return Ok(Vec::new());
11125 }
11126 // SET (option=value, …) — storage parameters.
11127 if matches!(self.peek(), Token::LParen) {
11128 self.consume_until_statement_boundary();
11129 return Ok(Vec::new());
11130 }
11131 Err(self.err(alloc::format!(
11132 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11133 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11134 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11135 )))
11136 }
11137 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11138 // not ignored: round 645 gave SPG the inheritance the
11139 // v7.37.18 no-op said it did not have.
11140 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11141 self.advance();
11142 let parent = self.expect_ident_like()?;
11143 self.consume_until_statement_boundary();
11144 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11145 parent,
11146 detach: false
11147 }])
11148 }
11149 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11150 // LEVEL SECURITY`, which has its own RLS arm below — without
11151 // the guard this swallowed NO FORCE as a no-op.
11152 Token::Ident(s)
11153 if s.eq_ignore_ascii_case("no")
11154 && !matches!(
11155 self.tokens.get(self.pos + 1),
11156 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11157 ) =>
11158 {
11159 self.advance();
11160 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11161 if k.eq_ignore_ascii_case("inherit"))
11162 {
11163 self.advance();
11164 let parent = self.expect_ident_like()?;
11165 self.consume_until_statement_boundary();
11166 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11167 parent,
11168 detach: true
11169 }]);
11170 }
11171 self.consume_until_statement_boundary();
11172 Ok(Vec::new())
11173 }
11174 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11175 // single-owner, so there is still nothing to record.
11176 //
11177 // v7.39 (round 652) — but the name now reaches the engine,
11178 // which refuses a role that does not exist as PG does. The
11179 // no-op was swallowing the whole statement, so a dump naming
11180 // a role this server never heard of restored clean and left
11181 // the table owned by whoever ran the restore.
11182 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11183 self.advance();
11184 if matches!(self.peek(), Token::To) {
11185 self.advance();
11186 }
11187 let role = self.expect_ident_like()?;
11188 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11189 role
11190 }])
11191 }
11192 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11193 // PG sets a hint; SPG doesn't have clustered storage, so the
11194 // hint itself stays a no-op.
11195 //
11196 // v7.39 (round 652) — the index name is checked now. PG
11197 // errors on one that does not exist, and swallowing that let
11198 // a typo'd CLUSTER ON pass silently.
11199 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11200 self.advance();
11201 // `ON` is a reserved token, not an ident.
11202 if !matches!(self.peek(), Token::On) {
11203 return Err(self.err(alloc::format!(
11204 "expected ON after CLUSTER, got {:?}",
11205 self.peek()
11206 )));
11207 }
11208 self.advance();
11209 let index = self.expect_ident_like()?;
11210 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11211 index: Some(index)
11212 }])
11213 }
11214 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11215 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11216 // what a logical decoder puts in the old-tuple image; SPG's
11217 // replication is SQL-text, so there is nothing to record.
11218 // Accept-and-no-op (it used to be a parse error).
11219 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11220 self.advance();
11221 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11222 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11223 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11224 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11225 {
11226 self.advance(); // IDENTITY
11227 self.advance(); // USING
11228 if matches!(self.peek(), Token::Index)
11229 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11230 {
11231 self.advance();
11232 }
11233 let index = self.expect_ident_like()?;
11234 self.consume_until_statement_boundary();
11235 return Ok(alloc::vec![
11236 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11237 ]);
11238 }
11239 self.consume_until_statement_boundary();
11240 Ok(Vec::new())
11241 }
11242 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11243 //
11244 // v7.39 (round 652) — it used to consume the statement and
11245 // return nothing, on the stated theory that SPG validated at
11246 // ADD CONSTRAINT time so there was never anything left to
11247 // validate. Measured against PG18, ADD CONSTRAINT did not
11248 // scan the existing rows at all — the comment described a
11249 // property SPG did not have, which is why nobody looked. Both
11250 // halves are real now: ADD scans unless told NOT VALID, and
11251 // this scans what NOT VALID skipped.
11252 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11253 self.advance();
11254 self.expect_keyword_ident("constraint")?;
11255 let name = self.expect_ident_like()?;
11256 Ok(alloc::vec![
11257 crate::ast::AlterTableTarget::ValidateConstraint { name }
11258 ])
11259 }
11260 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11261 // SET (option = value, …). PG uses it to clear per-table
11262 // storage params like fillfactor or autovacuum_*. SPG
11263 // engine-manages those parameters; accept-and-no-op.
11264 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11265 self.advance();
11266 self.consume_until_statement_boundary();
11267 Ok(Vec::new())
11268 }
11269 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11270 // type-of binding (PG 9.0+). SPG composite types
11271 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11272 // TABLE OF is rare and inverse of CREATE TABLE OF.
11273 // Accept-and-no-op until a customer dump round-trips it.
11274 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11275 self.advance();
11276 // v7.39 (round 710) — the type name is validated now.
11277 let type_name = self.expect_ident_like()?;
11278 self.consume_until_statement_boundary();
11279 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11280 type_name
11281 }])
11282 }
11283 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11284 // (reserved keyword) rather than Token::Ident("not"),
11285 // so it needs its own arm. Accept-and-no-op same as OF.
11286 Token::Not => {
11287 self.advance();
11288 self.consume_until_statement_boundary();
11289 Ok(Vec::new())
11290 }
11291 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11292 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11293 self.advance();
11294 self.expect_row_level_security()?;
11295 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11296 enabled: None,
11297 force: Some(true),
11298 }])
11299 }
11300 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11301 Token::Ident(s)
11302 if s.eq_ignore_ascii_case("no")
11303 && matches!(
11304 self.tokens.get(self.pos + 1),
11305 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11306 ) =>
11307 {
11308 self.advance(); // NO
11309 self.advance(); // FORCE
11310 self.expect_row_level_security()?;
11311 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11312 enabled: None,
11313 force: Some(false),
11314 }])
11315 }
11316 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11317 // (sets relrowsecurity). The guard requires the next token to be
11318 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11319 Token::Ident(s)
11320 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11321 && matches!(
11322 self.tokens.get(self.pos + 1),
11323 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11324 ) =>
11325 {
11326 let enabled = s.eq_ignore_ascii_case("enable");
11327 self.advance(); // ENABLE/DISABLE
11328 self.expect_row_level_security()?;
11329 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11330 enabled: Some(enabled),
11331 force: None,
11332 }])
11333 }
11334 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11335 self.advance();
11336 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11337 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11338 // emits. The same grammar CREATE TABLE already accepts
11339 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11340 // through the SAME parser — an ALTER-only copy would be a
11341 // second place for the two to drift.
11342 if self.peek_mysql_inline_key_start() {
11343 return Ok(match self.parse_mysql_inline_key()? {
11344 Some(c) => {
11345 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11346 }
11347 // FULLTEXT / SPATIAL parse and are accepted as a
11348 // no-op here exactly as they are inline.
11349 None => Vec::new(),
11350 });
11351 }
11352 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11353 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11354 // PRIMARY KEY this way; mysqldump emits both.
11355 // Peek-only dispatch (no advance) — `advance()`
11356 // destructively replaces consumed tokens with Eof,
11357 // so saved-pos restore would land on Eofs.
11358 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11359 {
11360 // The next-but-one ident is the constraint
11361 // name; the one after THAT is the kind.
11362 let kind_pos = self.pos + 2;
11363 let kind = self.tokens.get(kind_pos).cloned();
11364 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11365 {
11366 let fk = self.parse_table_level_fk()?;
11367 return Ok(alloc::vec![
11368 crate::ast::AlterTableTarget::AddForeignKey(fk)
11369 ]);
11370 }
11371 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11372 {
11373 self.advance(); // CONSTRAINT
11374 // v7.39 (read01 round 48) — keep the name; the engine
11375 // stores it now instead of dropping it on the floor.
11376 let con_name = self.expect_ident_like()?;
11377 self.advance(); // PRIMARY
11378 self.expect_keyword_ident("key")?;
11379 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11380 // v7.39 (round 711) — the ALTER form carries the
11381 // timing too (pg_dump writes it here).
11382 let (deferrable, initially_deferred) =
11383 self.consume_deferrable_clauses_timed()?;
11384 return Ok(alloc::vec![
11385 crate::ast::AlterTableTarget::AddTableConstraint(
11386 crate::ast::TableConstraint::PrimaryKey {
11387 name: Some(con_name),
11388 columns: cols,
11389 deferrable,
11390 initially_deferred,
11391 }
11392 )
11393 ]);
11394 }
11395 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11396 {
11397 self.advance(); // CONSTRAINT
11398 // v7.39 (read01 round 48) — keep the name.
11399 let con_name = self.expect_ident_like()?;
11400 // v7.22 (mailrs round-13 gap 6) — delegate so
11401 // the optional `NULLS [NOT] DISTINCT` modifier
11402 // parses here too (pg_dump emits the ALTER
11403 // form; semantics enforced by the engine
11404 // since v7.13).
11405 let mut uc = self.parse_table_level_unique()?;
11406 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11407 *name = Some(con_name);
11408 }
11409 return Ok(alloc::vec![
11410 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11411 ]);
11412 }
11413 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11414 {
11415 self.advance(); // CONSTRAINT
11416 // v7.39 (read01 round 48) — keep the name.
11417 let con_name = self.expect_ident_like()?;
11418 self.advance(); // CHECK
11419 if !matches!(self.peek(), Token::LParen) {
11420 return Err(self.err(alloc::format!(
11421 "expected '(' after CHECK, got {:?}", self.peek()
11422 )));
11423 }
11424 self.advance();
11425 let expr = self.parse_expr(0)?;
11426 if matches!(self.peek(), Token::RParen) {
11427 self.advance();
11428 }
11429 let not_valid = self.parse_not_valid_suffix();
11430 return Ok(alloc::vec![
11431 crate::ast::AlterTableTarget::AddTableConstraint(
11432 crate::ast::TableConstraint::Check {
11433 name: Some(con_name),
11434 expr,
11435 not_valid,
11436 }
11437 )
11438 ]);
11439 }
11440 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11441 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11442 // exclusion constraints via this ALTER form.
11443 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11444 {
11445 self.advance(); // CONSTRAINT
11446 let con_name = self.expect_ident_like()?;
11447 let mut ex = self.parse_table_level_exclude()?;
11448 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11449 *name = Some(con_name);
11450 }
11451 return Ok(alloc::vec![
11452 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11453 ]);
11454 }
11455 // Unknown kind — fall through to FK path which
11456 // produces a descriptive parse error.
11457 }
11458 let is_fk = matches!(
11459 self.peek(),
11460 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11461 || s.eq_ignore_ascii_case("foreign")
11462 );
11463 if is_fk {
11464 let fk = self.parse_table_level_fk()?;
11465 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11466 }
11467 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11468 // (no CONSTRAINT prefix) — same dispatch.
11469 match self.peek().clone() {
11470 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11471 self.advance();
11472 self.expect_keyword_ident("key")?;
11473 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11474 let (deferrable, initially_deferred) =
11475 self.consume_deferrable_clauses_timed()?;
11476 return Ok(alloc::vec![
11477 crate::ast::AlterTableTarget::AddTableConstraint(
11478 crate::ast::TableConstraint::PrimaryKey {
11479 name: None,
11480 columns: cols,
11481 deferrable,
11482 initially_deferred,
11483 }
11484 )
11485 ]);
11486 }
11487 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11488 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11489 let uc = self.parse_table_level_unique()?;
11490 return Ok(alloc::vec![
11491 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11492 ]);
11493 }
11494 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11495 // prefix). The other three bare forms were here and
11496 // this one was not, so it fell through to the column
11497 // path and came back as "unexpected reserved keyword
11498 // 'check' at start of column definition".
11499 _ if self.peek_table_level_check_start() => {
11500 let chk = self.parse_table_level_check()?;
11501 let not_valid = self.parse_not_valid_suffix();
11502 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11503 unreachable!("parse_table_level_check returns Check")
11504 };
11505 return Ok(alloc::vec![
11506 crate::ast::AlterTableTarget::AddTableConstraint(
11507 crate::ast::TableConstraint::Check {
11508 name: None,
11509 expr,
11510 not_valid,
11511 }
11512 )
11513 ]);
11514 }
11515 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11516 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11517 let ex = self.parse_table_level_exclude()?;
11518 return Ok(alloc::vec![
11519 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11520 ]);
11521 }
11522 _ => {}
11523 }
11524 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11525 self.advance();
11526 }
11527 let mut if_not_exists = false;
11528 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11529 self.advance();
11530 if !matches!(self.peek(), Token::Not) {
11531 return Err(self.err(alloc::format!(
11532 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11533 self.peek()
11534 )));
11535 }
11536 self.advance();
11537 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11538 return Err(self.err(alloc::format!(
11539 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11540 self.peek()
11541 )));
11542 }
11543 self.advance();
11544 if_not_exists = true;
11545 }
11546 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11547 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11548 // returns ColumnDef + an optional inline FK.
11549 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11550 let col_name = column.name.clone();
11551 // v7.39.9 — MySQL says where the column goes.
11552 let position = self.parse_column_position();
11553 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11554 column,
11555 if_not_exists,
11556 position,
11557 }];
11558 if let Some(mut fk) = col_level_fk {
11559 if fk.columns.is_empty() {
11560 fk.columns.push(col_name);
11561 }
11562 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11563 }
11564 Ok(out)
11565 }
11566 Token::Drop => {
11567 self.advance();
11568 // v7.13.3 — dispatch on the next token. mailrs round-7
11569 // S8 closed DROP COLUMN; round-6 S7 closed
11570 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11571 // RESTRICT modifiers.
11572 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11573 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11574 let subject = match self.peek() {
11575 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11576 self.advance();
11577 "constraint"
11578 }
11579 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11580 self.advance();
11581 "column"
11582 }
11583 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11584 // `INDEX` lexes as the reserved Token::Index, so it is
11585 // unambiguous. `KEY` is a plain ident, and PG allows a
11586 // column literally named "key", so only read it as the
11587 // keyword when a name follows it.
11588 Token::Index => {
11589 self.advance();
11590 "index"
11591 }
11592 Token::Ident(s)
11593 if s.eq_ignore_ascii_case("key")
11594 && matches!(
11595 self.tokens.get(self.pos + 1),
11596 Some(Token::Ident(_) | Token::QuotedIdent(_))
11597 ) =>
11598 {
11599 self.advance();
11600 "index"
11601 }
11602 // PG-canonical bare `DROP <col>` without COLUMN
11603 // keyword is also valid; treat any other ident
11604 // as the column name.
11605 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11606 other => {
11607 return Err(self.err(alloc::format!(
11608 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11609 )));
11610 }
11611 };
11612 let mut if_exists = false;
11613 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11614 let n1 = self.tokens.get(self.pos + 1);
11615 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11616 self.advance();
11617 self.advance();
11618 if_exists = true;
11619 }
11620 }
11621 let name = self.expect_ident_like()?;
11622 let mut cascade = false;
11623 if matches!(
11624 self.peek(),
11625 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11626 || s.eq_ignore_ascii_case("restrict")
11627 ) {
11628 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11629 {
11630 cascade = true;
11631 }
11632 self.advance();
11633 }
11634 if subject == "index" {
11635 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11636 name,
11637 if_exists,
11638 }])
11639 } else if subject == "constraint" {
11640 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11641 name,
11642 if_exists,
11643 }])
11644 } else {
11645 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11646 column: name,
11647 if_exists,
11648 cascade,
11649 }])
11650 }
11651 }
11652 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11653 self.advance();
11654 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11655 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11656 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11657 // immediately; accept-and-no-op.
11658 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11659 self.advance();
11660 self.consume_until_statement_boundary();
11661 return Ok(Vec::new());
11662 }
11663 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11664 self.advance();
11665 }
11666 let col_name = self.expect_ident_like()?;
11667 match self.peek() {
11668 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11669 self.advance();
11670 }
11671 // v7.14.0 — pg_dump emits BIGSERIAL via
11672 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11673 // nextval('seq')` (the sequence is created
11674 // separately). SPG's BIGSERIAL already uses
11675 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11676 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11677 // engine no-ops by consuming the tail.
11678 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11679 // v7.22 (round-13 T2) — `SET DEFAULT
11680 // nextval('…')` is how pg_dump spells a
11681 // SERIAL column (plain integer in CREATE
11682 // TABLE + this ALTER). It used to be
11683 // swallowed as a no-op, which silently
11684 // STRIPPED auto-increment from imported
11685 // schemas — the first post-import INSERT
11686 // without an explicit id then violated NOT
11687 // NULL. Lower it to the auto-increment
11688 // marker instead.
11689 let is_default_nextval =
11690 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11691 && matches!(
11692 self.tokens.get(self.pos + 2),
11693 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11694 );
11695 if is_default_nextval {
11696 let seq_name = self.scan_sequence_name_until_boundary();
11697 return Ok(alloc::vec![
11698 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11699 column: col_name,
11700 seq_name,
11701 }
11702 ]);
11703 }
11704 // v7.37.18 (18.1 + 18.2) — proper lowering.
11705 self.advance(); // consume "set"
11706 match self.peek().clone() {
11707 Token::Default => {
11708 self.advance();
11709 let default_expr = self.parse_expr(0)?;
11710 return Ok(alloc::vec![
11711 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11712 column: col_name,
11713 default_expr,
11714 }
11715 ]);
11716 }
11717 Token::Not => {
11718 self.advance();
11719 if !matches!(self.peek(), Token::Null) {
11720 return Err(self.err(alloc::format!(
11721 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11722 self.peek()
11723 )));
11724 }
11725 self.advance();
11726 return Ok(alloc::vec![
11727 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11728 column: col_name,
11729 }
11730 ]);
11731 }
11732 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11733 // stored generated column's expression and
11734 // recompute existing rows.
11735 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11736 self.advance(); // EXPRESSION
11737 if matches!(self.peek(), Token::As) {
11738 self.advance();
11739 }
11740 let expr = self.parse_expr(0)?;
11741 return Ok(alloc::vec![
11742 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11743 column: col_name,
11744 expr,
11745 }
11746 ]);
11747 }
11748 other => {
11749 // Other SET subjects (STATISTICS,
11750 // STORAGE, COMPRESSION, …) stay no-ops —
11751 // storage hints with no SPG semantics.
11752 let _ = other;
11753 self.consume_until_statement_boundary();
11754 return Ok(Vec::new());
11755 }
11756 }
11757 }
11758 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11759 self.advance(); // consume "drop"
11760 return self.parse_alter_column_drop_tail(col_name);
11761 }
11762 Token::Drop => {
11763 self.advance(); // consume Drop token
11764 return self.parse_alter_column_drop_tail(col_name);
11765 }
11766 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11767 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11768 // GENERATED { ALWAYS | BY DEFAULT } AS
11769 // IDENTITY ( … )`: pg_dump's spelling for
11770 // identity columns. Same auto-increment
11771 // lowering as the nextval default; the
11772 // sequence options inside the parens are
11773 // no-ops under SPG's max+1 semantics.
11774 let is_generated = matches!(
11775 self.tokens.get(self.pos + 1),
11776 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11777 );
11778 if !is_generated {
11779 return Err(self.err(alloc::format!(
11780 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11781 self.tokens.get(self.pos + 1)
11782 )));
11783 }
11784 let seq_name = self.scan_sequence_name_until_boundary();
11785 return Ok(alloc::vec![
11786 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11787 column: col_name,
11788 seq_name,
11789 }
11790 ]);
11791 }
11792 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11793 // column: floor the next allocated value at n (bare
11794 // RESTART = restart from the start value, 1).
11795 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11796 self.advance();
11797 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11798 {
11799 self.advance();
11800 let neg = if matches!(self.peek(), Token::Minus) {
11801 self.advance();
11802 true
11803 } else {
11804 false
11805 };
11806 match self.advance() {
11807 Token::Integer(v) => Some(if neg { -v } else { v }),
11808 other => {
11809 return Err(self.err(alloc::format!(
11810 "expected integer after RESTART WITH, got {other:?}"
11811 )));
11812 }
11813 }
11814 } else {
11815 None
11816 };
11817 return Ok(alloc::vec![
11818 crate::ast::AlterTableTarget::AlterColumnRestart {
11819 column: col_name,
11820 with,
11821 }
11822 ]);
11823 }
11824 other => {
11825 return Err(self.err(alloc::format!(
11826 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11827 )));
11828 }
11829 }
11830 // v7.39 (round 713) — the type parser has consumed a
11831 // trailing `COLLATE <name>` since Phase 2.5, and
11832 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11833 // TYPE text COLLATE "C"` parsed clean and changed
11834 // nothing. Keep the clause; the engine re-collates.
11835 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11836 self.parse_type_with_implied_flags()?;
11837 let collation = if coll_explicit {
11838 coll_name.map(|n| (coll, n))
11839 } else {
11840 None
11841 };
11842 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11843 {
11844 self.advance();
11845 Some(self.parse_expr(0)?)
11846 } else {
11847 None
11848 };
11849 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11850 column: col_name,
11851 new_type,
11852 using,
11853 collation,
11854 }])
11855 }
11856 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11857 // PG also supports `RENAME TO new_table` for table-name
11858 // rename; that surface is deferred (pg_dump never emits
11859 // it). If the first post-RENAME ident is `TO`, the user
11860 // is asking for table rename — error with a clear
11861 // message rather than misparsing `TO` as a column name.
11862 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11863 self.advance();
11864 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11865 // table-name rename (mailrs round-10 A.5 — used
11866 // by migrate-042's `RENAME TO email_contacts`).
11867 // `TO` lexes as Token::To.
11868 if matches!(self.peek(), Token::To)
11869 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11870 {
11871 self.advance();
11872 let new = self.expect_ident_like()?;
11873 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11874 new,
11875 }]);
11876 }
11877 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11878 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11879 self.advance();
11880 let old = self.expect_ident_like()?;
11881 if matches!(self.peek(), Token::To) {
11882 self.advance();
11883 } else {
11884 self.expect_keyword_ident("to")?;
11885 }
11886 let new = self.expect_ident_like()?;
11887 return Ok(alloc::vec![
11888 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11889 ]);
11890 }
11891 // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11892 // PostgreSQL renames an index with its own top-level
11893 // `ALTER INDEX`, so this spelling had nowhere to go and
11894 // answered 1064; MySQL 9.7.2 parses it and answers 1176
11895 // when the key is not there.
11896 if matches!(self.peek(), Token::Index)
11897 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11898 {
11899 self.advance();
11900 let old = self.expect_ident_like()?;
11901 if matches!(self.peek(), Token::To) {
11902 self.advance();
11903 } else {
11904 self.expect_keyword_ident("to")?;
11905 }
11906 let new = self.expect_ident_like()?;
11907 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11908 old,
11909 new,
11910 }]);
11911 }
11912 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11913 self.advance();
11914 }
11915 let old = self.expect_ident_like()?;
11916 // `TO` is a reserved keyword token; accept both
11917 // Token::To and Token::Ident("to") for consistency.
11918 if matches!(self.peek(), Token::To) {
11919 self.advance();
11920 } else {
11921 self.expect_keyword_ident("to")?;
11922 }
11923 let new = self.expect_ident_like()?;
11924 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11925 old,
11926 new,
11927 }])
11928 }
11929 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11930 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11931 // every data block with these. Real disable semantics —
11932 // not no-op — because reload correctness assumes the
11933 // triggers don't fire (rows already carry their
11934 // computed values from prod).
11935 Token::Ident(s)
11936 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11937 {
11938 let enabled = s.eq_ignore_ascii_case("enable");
11939 self.advance();
11940 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11941 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11942 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11943 // pg_dump output) — anything else falls through to
11944 // the catch-all error below.
11945 // v7.22 (round-13 T3) — mysqldump wraps every data
11946 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11947 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11948 // maintains indexes incrementally — engine no-op.
11949 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11950 self.advance();
11951 return Ok(Vec::new());
11952 }
11953 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11954 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11955 // to gate triggers on session_replication_role; SPG
11956 // has no replica role, so the prefix is consumed and
11957 // treated identically to the plain ENABLE/DISABLE
11958 // TRIGGER form.
11959 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11960 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11961 {
11962 self.advance();
11963 }
11964 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11965 return Err(self.err(alloc::format!(
11966 "expected TRIGGER after {}, got {:?}",
11967 if enabled { "ENABLE" } else { "DISABLE" },
11968 self.peek()
11969 )));
11970 }
11971 self.advance();
11972 // `ALL` lexes as Token::All (reserved); also
11973 // accept Token::Ident("all") for symmetry.
11974 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11975 // TRIGGER selectors. USER (= all user triggers) is
11976 // semantically ALL here; REPLICA / ALWAYS gate on
11977 // session_replication_role which SPG doesn't track.
11978 // All map to TriggerSelector::All.
11979 let which = if matches!(self.peek(), Token::All)
11980 || matches!(self.peek(), Token::Ident(s)
11981 if s.eq_ignore_ascii_case("all")
11982 || s.eq_ignore_ascii_case("user")
11983 || s.eq_ignore_ascii_case("replica")
11984 || s.eq_ignore_ascii_case("always"))
11985 {
11986 self.advance();
11987 crate::ast::TriggerSelector::All
11988 } else {
11989 let name = self.expect_ident_like()?;
11990 crate::ast::TriggerSelector::Named(name)
11991 };
11992 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11993 which,
11994 enabled,
11995 }])
11996 }
11997 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11998 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11999 self.advance();
12000 if !matches!(self.peek(), Token::Partition)
12001 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12002 if s.eq_ignore_ascii_case("partition"))
12003 {
12004 return Err(self.err(alloc::format!(
12005 "expected PARTITION after ATTACH, got {:?}",
12006 self.peek()
12007 )));
12008 }
12009 self.advance();
12010 let child = self.expect_ident_like()?;
12011 let bounds = self.parse_partition_bounds_tail()?;
12012 Ok(alloc::vec![
12013 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12014 ])
12015 }
12016 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12017 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12018 self.advance();
12019 if !matches!(self.peek(), Token::Partition)
12020 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12021 if s.eq_ignore_ascii_case("partition"))
12022 {
12023 return Err(self.err(alloc::format!(
12024 "expected PARTITION after DETACH, got {:?}",
12025 self.peek()
12026 )));
12027 }
12028 self.advance();
12029 let child = self.expect_ident_like()?;
12030 let mut concurrently = false;
12031 let mut finalize = false;
12032 loop {
12033 match self.peek().clone() {
12034 Token::Ident(s) | Token::QuotedIdent(s)
12035 if s.eq_ignore_ascii_case("concurrently") =>
12036 {
12037 self.advance();
12038 concurrently = true;
12039 }
12040 Token::Ident(s) | Token::QuotedIdent(s)
12041 if s.eq_ignore_ascii_case("finalize") =>
12042 {
12043 self.advance();
12044 finalize = true;
12045 }
12046 _ => break,
12047 }
12048 }
12049 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12050 child,
12051 concurrently,
12052 finalize,
12053 }])
12054 }
12055 other => Err(self.err(alloc::format!(
12056 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12057 ))),
12058 }
12059 }
12060
12061 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12062 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12063 /// TABLE … ATTACH PARTITION. Shares the same grammar as
12064 /// `parse_partition_of_tail`'s bounds branch.
12065 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12066 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12067 /// lowering each to the respective AlterTableTarget. Any
12068 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12069 /// no-op via consume_until_statement_boundary.
12070 fn parse_alter_column_drop_tail(
12071 &mut self,
12072 col_name: String,
12073 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12074 match self.peek().clone() {
12075 Token::Default => {
12076 self.advance();
12077 Ok(alloc::vec![
12078 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12079 ])
12080 }
12081 Token::Not => {
12082 self.advance();
12083 if !matches!(self.peek(), Token::Null) {
12084 return Err(self.err(alloc::format!(
12085 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12086 self.peek()
12087 )));
12088 }
12089 self.advance();
12090 Ok(alloc::vec![
12091 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12092 ])
12093 }
12094 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12095 // generated column into a plain column.
12096 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12097 self.advance();
12098 // v7.39 (round 187, U10) — IF EXISTS was consumed but
12099 // dropped, so the engine still errored on a plain
12100 // column; PG's semantics are NOTICE + skip.
12101 let mut if_exists = false;
12102 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12103 self.advance();
12104 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12105 self.advance();
12106 if_exists = true;
12107 }
12108 }
12109 Ok(alloc::vec![
12110 crate::ast::AlterTableTarget::AlterColumnDropExpression {
12111 column: col_name,
12112 if_exists,
12113 }
12114 ])
12115 }
12116 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12117 // identity column into a plain column.
12118 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12119 self.advance();
12120 let mut if_exists = false;
12121 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12122 self.advance();
12123 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12124 self.advance();
12125 if_exists = true;
12126 }
12127 }
12128 Ok(alloc::vec![
12129 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12130 column: col_name,
12131 if_exists,
12132 }
12133 ])
12134 }
12135 _ => {
12136 self.consume_until_statement_boundary();
12137 Ok(Vec::new())
12138 }
12139 }
12140 }
12141
12142 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12143 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12144 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12145 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12146 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12147 let mut opts = crate::ast::CopyOptions::default();
12148 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12149 return Ok(opts);
12150 }
12151 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12152 self.advance();
12153 }
12154 if matches!(self.peek(), Token::LParen) {
12155 self.advance();
12156 loop {
12157 self.parse_one_copy_option(&mut opts)?;
12158 match self.peek() {
12159 Token::Comma => {
12160 self.advance();
12161 }
12162 Token::RParen => {
12163 self.advance();
12164 break;
12165 }
12166 other => {
12167 return Err(self.err(alloc::format!(
12168 "expected ',' or ')' in COPY options, got {other:?}"
12169 )));
12170 }
12171 }
12172 }
12173 } else {
12174 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12175 self.parse_one_copy_option(&mut opts)?;
12176 }
12177 }
12178 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12179 return Err(self.err(alloc::format!(
12180 "unexpected token after COPY options: {:?}",
12181 self.peek()
12182 )));
12183 }
12184 Ok(opts)
12185 }
12186
12187 fn parse_one_copy_option(
12188 &mut self,
12189 opts: &mut crate::ast::CopyOptions,
12190 ) -> Result<(), ParseError> {
12191 use crate::ast::CopyFormat;
12192 // The option keyword. NULL lexes as its own token; the rest are
12193 // bare identifiers.
12194 let kw = match self.advance() {
12195 Token::Null => alloc::string::String::from("NULL"),
12196 Token::Ident(s) => s.to_uppercase(),
12197 other => {
12198 return Err(self.err(alloc::format!(
12199 "expected a COPY option keyword, got {other:?}"
12200 )));
12201 }
12202 };
12203 match kw.as_str() {
12204 "FORMAT" => {
12205 let fmt = self.expect_ident_like()?;
12206 match fmt.to_ascii_uppercase().as_str() {
12207 "CSV" => opts.format = CopyFormat::Csv,
12208 "TEXT" => opts.format = CopyFormat::Text,
12209 other => {
12210 return Err(self.err(alloc::format!(
12211 "COPY format \"{}\" not recognized",
12212 other.to_ascii_lowercase()
12213 )));
12214 }
12215 }
12216 }
12217 // Legacy bare format keywords.
12218 "CSV" => opts.format = CopyFormat::Csv,
12219 "TEXT" => opts.format = CopyFormat::Text,
12220 "HEADER" => {
12221 opts.header = match self.peek() {
12222 Token::True => {
12223 self.advance();
12224 true
12225 }
12226 Token::False => {
12227 self.advance();
12228 false
12229 }
12230 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12231 self.advance();
12232 true
12233 }
12234 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12235 self.advance();
12236 false
12237 }
12238 // Bare HEADER (no boolean) means HEADER true.
12239 _ => true,
12240 };
12241 }
12242 // r1066 (7.38 S5.1) — pgbench 14+ loads with
12243 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12244 // vacuum bookkeeping on a freshly created/truncated
12245 // table; SPG's per-statement visibility makes it a
12246 // faithful no-op, and rejecting it aborted `pgbench -i`
12247 // against the drop-in. Accept ON/OFF/bare, change nothing.
12248 "FREEZE" => match self.peek() {
12249 Token::True | Token::False => {
12250 self.advance();
12251 }
12252 Token::Ident(s)
12253 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12254 {
12255 self.advance();
12256 }
12257 _ => {}
12258 },
12259 "DELIMITER" | "QUOTE" | "ESCAPE" => {
12260 let s = match self.advance() {
12261 Token::String(s) => s,
12262 other => {
12263 return Err(self.err(alloc::format!(
12264 "COPY {kw} expects a single-character string, got {other:?}"
12265 )));
12266 }
12267 };
12268 // v7.39 (round 247) — PG's wording (0A000), keyword in
12269 // lowercase: "COPY delimiter must be a single one-byte
12270 // character".
12271 let one_byte_err = || {
12272 self.err(alloc::format!(
12273 "COPY {} must be a single one-byte character",
12274 kw.to_ascii_lowercase()
12275 ))
12276 };
12277 let mut chars = s.chars();
12278 let c = chars.next().ok_or_else(one_byte_err)?;
12279 if chars.next().is_some() || c.len_utf8() != 1 {
12280 return Err(one_byte_err());
12281 }
12282 match kw.as_str() {
12283 "DELIMITER" => opts.delimiter = Some(c),
12284 "QUOTE" => opts.quote = Some(c),
12285 _ => opts.escape = Some(c),
12286 }
12287 }
12288 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12289 "FORCE_QUOTE" => {
12290 if matches!(self.peek(), Token::Star) {
12291 self.advance();
12292 opts.force_quote = Some(Vec::new());
12293 } else {
12294 if !matches!(self.peek(), Token::LParen) {
12295 return Err(self.err(alloc::format!(
12296 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12297 self.peek()
12298 )));
12299 }
12300 self.advance();
12301 let mut cols = Vec::new();
12302 loop {
12303 cols.push(self.expect_ident_like()?);
12304 match self.peek() {
12305 Token::Comma => {
12306 self.advance();
12307 }
12308 Token::RParen => {
12309 self.advance();
12310 break;
12311 }
12312 other => {
12313 return Err(self.err(alloc::format!(
12314 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12315 )));
12316 }
12317 }
12318 }
12319 opts.force_quote = Some(cols);
12320 }
12321 }
12322 "NULL" => {
12323 opts.null_str = Some(match self.advance() {
12324 Token::String(s) => s,
12325 other => {
12326 return Err(self.err(alloc::format!(
12327 "COPY NULL expects a quoted string, got {other:?}"
12328 )));
12329 }
12330 });
12331 }
12332 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12333 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12334 // FORCE_NULL too.
12335 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12336 let cols = self.parse_copy_column_list(&kw)?;
12337 if kw == "FORCE_NOT_NULL" {
12338 opts.force_not_null = Some(cols);
12339 } else {
12340 opts.force_null = Some(cols);
12341 }
12342 }
12343 other => {
12344 // PG's wording, lowercased option name.
12345 return Err(self.err(alloc::format!(
12346 "option \"{}\" not recognized",
12347 other.to_ascii_lowercase()
12348 )));
12349 }
12350 }
12351 Ok(())
12352 }
12353
12354 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12355 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12356 /// is the `*` spelling.
12357 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12358 if matches!(self.peek(), Token::Star) {
12359 self.advance();
12360 return Ok(Vec::new());
12361 }
12362 if !matches!(self.peek(), Token::LParen) {
12363 return Err(self.err(alloc::format!(
12364 "expected '(' or '*' after {kw}, got {:?}",
12365 self.peek()
12366 )));
12367 }
12368 self.advance();
12369 let mut cols = Vec::new();
12370 loop {
12371 cols.push(self.expect_ident_like()?);
12372 match self.peek() {
12373 Token::Comma => {
12374 self.advance();
12375 }
12376 Token::RParen => {
12377 self.advance();
12378 break;
12379 }
12380 other => {
12381 return Err(self.err(alloc::format!(
12382 "expected ',' or ')' in {kw} list, got {other:?}"
12383 )));
12384 }
12385 }
12386 }
12387 Ok(cols)
12388 }
12389
12390 fn parse_partition_bounds_tail(
12391 &mut self,
12392 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12393 use crate::ast::PartitionOfBoundsAst;
12394 match self.peek() {
12395 Token::Default => {
12396 self.advance();
12397 Ok(PartitionOfBoundsAst::Default)
12398 }
12399 Token::For => {
12400 self.advance();
12401 if !matches!(self.peek(), Token::Values) {
12402 return Err(
12403 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12404 );
12405 }
12406 self.advance();
12407 let want_with = matches!(
12408 self.peek(),
12409 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12410 );
12411 if want_with {
12412 self.advance();
12413 if !matches!(self.peek(), Token::LParen) {
12414 return Err(self.err(format!(
12415 "expected '(' after FOR VALUES WITH, got {:?}",
12416 self.peek()
12417 )));
12418 }
12419 self.advance();
12420 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12421 loop {
12422 let key = self.expect_ident_like()?;
12423 let n = match self.peek().clone() {
12424 Token::Integer(v) if u32::try_from(v).is_ok() => {
12425 self.advance();
12426 v as u32
12427 }
12428 other => {
12429 return Err(self.err(format!(
12430 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12431 )));
12432 }
12433 };
12434 match key.to_ascii_uppercase().as_str() {
12435 "MODULUS" => modulus = Some(n),
12436 "REMAINDER" => remainder = Some(n),
12437 other => {
12438 return Err(self.err(format!(
12439 "FOR VALUES WITH: unknown key {other:?}; \
12440 expected MODULUS or REMAINDER"
12441 )));
12442 }
12443 }
12444 match self.peek() {
12445 Token::Comma => {
12446 self.advance();
12447 }
12448 Token::RParen => {
12449 self.advance();
12450 break;
12451 }
12452 other => {
12453 return Err(self.err(format!(
12454 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12455 )));
12456 }
12457 }
12458 }
12459 let modulus = modulus
12460 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12461 let remainder = remainder.ok_or_else(|| {
12462 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12463 })?;
12464 if modulus == 0 {
12465 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12466 }
12467 if remainder >= modulus {
12468 return Err(self.err(format!(
12469 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12470 )));
12471 }
12472 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12473 }
12474 match self.peek() {
12475 Token::From => {
12476 self.advance();
12477 let lower = Box::new(self.parse_partition_bound_expr()?);
12478 if !matches!(self.peek(), Token::To) {
12479 return Err(self.err(format!(
12480 "expected TO after FROM (...), got {:?}",
12481 self.peek()
12482 )));
12483 }
12484 self.advance();
12485 let upper = Box::new(self.parse_partition_bound_expr()?);
12486 Ok(PartitionOfBoundsAst::Range { lower, upper })
12487 }
12488 Token::In => {
12489 self.advance();
12490 if !matches!(self.peek(), Token::LParen) {
12491 return Err(self.err(format!(
12492 "expected '(' after FOR VALUES IN, got {:?}",
12493 self.peek()
12494 )));
12495 }
12496 self.advance();
12497 let mut values = Vec::new();
12498 loop {
12499 values.push(self.parse_expr(0)?);
12500 match self.peek() {
12501 Token::Comma => {
12502 self.advance();
12503 }
12504 Token::RParen => {
12505 self.advance();
12506 break;
12507 }
12508 other => {
12509 return Err(self.err(format!(
12510 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12511 )));
12512 }
12513 }
12514 }
12515 if values.is_empty() {
12516 return Err(
12517 self.err("FOR VALUES IN requires at least one literal".to_string())
12518 );
12519 }
12520 Ok(PartitionOfBoundsAst::List { values })
12521 }
12522 other => Err(self.err(format!(
12523 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12524 ))),
12525 }
12526 }
12527 other => Err(self.err(format!(
12528 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12529 ))),
12530 }
12531 }
12532
12533 /// v7.16.2 — peek for `information_schema.<tbl>` /
12534 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12535 /// three tokens + return a synthetic table name the engine's
12536 /// SELECT path recognises as a virtual view. Returns `None`
12537 /// when the head doesn't look like a meta-qualified name.
12538 /// Used by `parse_table_ref` to bypass the
12539 /// `expect_ident_like` schema-strip for these specific PG
12540 /// meta schemas (mailrs round-10 A.3).
12541 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12542 // Extract the schema name. Must be a plain ident token.
12543 let schema = match self.tokens.get(self.pos) {
12544 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12545 _ => return None,
12546 };
12547 // Dot.
12548 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12549 return None;
12550 }
12551 // The table-side ident may lex as a reserved keyword
12552 // (e.g. `Token::Tables`). Tolerate the common ones via a
12553 // helper that reads the trailing token's underlying name.
12554 let tbl = match self.tokens.get(self.pos + 2)? {
12555 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12556 Token::Tables => "tables".to_string(),
12557 // Other PG meta table names that may collide with
12558 // reserved keywords land here as needed.
12559 _ => return None,
12560 };
12561 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12562 // names so the synthetic name doesn't double-prefix
12563 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12564 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12565 ("__spg_info_", tbl.to_ascii_lowercase())
12566 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12567 // v7.39 (round 541) — only the catalogs SPG actually
12568 // synthesises are rewritten, which is what the BARE path
12569 // has always checked. Anything else keeps its own name and
12570 // takes the ordinary route: `pg_stat_activity` and friends
12571 // resolve through meta_view_result, and a name that is no
12572 // catalog at all gets PG's "relation does not exist"
12573 // instead of a message about a view SPG cannot materialise.
12574 let lowered = tbl.to_ascii_lowercase();
12575 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12576 self.advance(); // schema
12577 self.advance(); // dot
12578 self.advance(); // tbl
12579 return Some((lowered.clone(), lowered));
12580 }
12581 let bare = lowered
12582 .strip_prefix("pg_")
12583 .map(alloc::string::String::from)
12584 .unwrap_or(lowered);
12585 ("__spg_pg_", bare)
12586 } else if schema.eq_ignore_ascii_case("mysql") {
12587 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12588 // (`mysql.user`, `mysql.db`). Same synthetic-name
12589 // shape as pg_catalog.
12590 ("__spg_mysql_", tbl.to_ascii_lowercase())
12591 } else {
12592 return None;
12593 };
12594 self.advance(); // schema
12595 self.advance(); // dot
12596 self.advance(); // tbl
12597 Some((
12598 alloc::format!("{prefix}{normalised}"),
12599 tbl.to_ascii_lowercase(),
12600 ))
12601 }
12602
12603 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12604 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12605 /// implicit front of every search_path, so a bare reference to a
12606 /// known catalog table always means the catalog table. Only the
12607 /// names the engine actually synthesises are recognised — any
12608 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12609 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12610 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12611 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12612 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12613 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12614 // through the meta_view_result path instead, and already resolve
12615 // bare — they must NOT be listed here or the __spg_ rewrite would
12616 // mis-target them.)
12617 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12618 let name = match self.tokens.get(self.pos) {
12619 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12620 _ => return None,
12621 };
12622 // A following dot means this ident is a schema qualifier,
12623 // not a table name — let the qualified path handle it.
12624 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12625 return None;
12626 }
12627 if !PG_META_TABLES.contains(&name.as_str()) {
12628 return None;
12629 }
12630 self.advance();
12631 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12632 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12633 }
12634
12635 /// Consume a bare ident if its lowercase matches `kw`, else err.
12636 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12637 /// Peeks only; the caller advances.
12638 fn peek_keyword_ident(&self, kw: &str) -> bool {
12639 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12640 }
12641
12642 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12643 match self.advance() {
12644 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12645 other => Err(ParseError {
12646 message: format!("expected {kw:?}, got {other:?}"),
12647 token_pos: self.consumed_pos(),
12648 }),
12649 }
12650 }
12651
12652 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12653 /// literal (`'foo'`) — same shape used by CREATE USER for the
12654 /// username slot.
12655 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12656 match self.advance() {
12657 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12658 other => Err(ParseError {
12659 message: format!("expected identifier or string, got {other:?}"),
12660 token_pos: self.consumed_pos(),
12661 }),
12662 }
12663 }
12664
12665 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12666 match self.advance() {
12667 Token::String(s) => Ok(s),
12668 other => Err(ParseError {
12669 message: format!("expected quoted string, got {other:?}"),
12670 token_pos: self.consumed_pos(),
12671 }),
12672 }
12673 }
12674
12675 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12676 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12677 // subqueries recurse through here without passing
12678 // parse_expr; share the same nesting budget.
12679 self.enter_nested()?;
12680 let r = self.parse_select_stmt_inner();
12681 self.nest_depth -= 1;
12682 r
12683 }
12684
12685 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12686 // Caller dispatches on Token::Select; the inner helper handles
12687 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12688 // get a fresh bare-select parse and may not have their own ORDER
12689 // BY / LIMIT.
12690 let mut head = self.parse_bare_select()?;
12691 let into = self.pending_select_into.take();
12692 self.parse_setop_chain_into(&mut head)?;
12693 self.parse_select_tail_into(&mut head)?;
12694 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12695 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12696 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12697 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12698 // to the body, as it does in PostgreSQL.
12699 if let Some((name, temporary)) = into {
12700 return Ok(Statement::CreateMaterializedView(
12701 crate::ast::CreateMaterializedViewStatement {
12702 temporary,
12703 name,
12704 if_not_exists: false,
12705 columns: Vec::new(),
12706 body: head,
12707 with_data: true,
12708 as_plain_table: true,
12709 },
12710 ));
12711 }
12712 Ok(Statement::Select(head))
12713 }
12714
12715 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12716 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12717 /// token), and INTERSECT [ALL] (a bare ident — it was never
12718 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12719 /// tighter than UNION / EXCEPT — the executor folds the chain
12720 /// left-to-right, which is already correct for LEADING
12721 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12722 /// pair nests into that previous peer, so A UNION B INTERSECT C
12723 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12724 /// groups.
12725 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12726 // A parenthesized group arrives with its own (already
12727 // regrouped) unions on `head`; only the pairs THIS chain
12728 // appends participate in the precedence regroup below —
12729 // nesting an outer INTERSECT into a group-internal peer
12730 // would dissolve the explicit grouping.
12731 let boundary = head.unions.len();
12732 loop {
12733 let base = match self.peek() {
12734 Token::Union => UnionKind::Distinct,
12735 Token::Except => UnionKind::Except,
12736 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12737 _ => break,
12738 };
12739 self.advance();
12740 let kind = if matches!(self.peek(), Token::All) {
12741 self.advance();
12742 match base {
12743 UnionKind::Distinct => UnionKind::All,
12744 UnionKind::Except => UnionKind::ExceptAll,
12745 _ => UnionKind::IntersectAll,
12746 }
12747 } else {
12748 base
12749 };
12750 let peer = self.parse_bare_select()?;
12751 head.unions.push((kind, peer));
12752 }
12753 let mut pairs = core::mem::take(&mut head.unions);
12754 let tail = pairs.split_off(boundary);
12755 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12756 for (kind, peer) in tail {
12757 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12758 // An intersect nests into the previous element of THIS
12759 // chain only; with no new previous element it stays at
12760 // the outer level (the left fold applies it to the
12761 // whole head, group included).
12762 match (
12763 is_intersect,
12764 regrouped.len() > boundary,
12765 regrouped.last_mut(),
12766 ) {
12767 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12768 _ => regrouped.push((kind, peer)),
12769 }
12770 }
12771 head.unions = regrouped;
12772 Ok(())
12773 }
12774
12775 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12776 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12777 /// the top-level bare VALUES statement reuses it verbatim.
12778 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12779 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12780 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12781 /// where the grouping-set universe is still in scope.
12782 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12783 if !matches!(self.peek(), Token::Order) {
12784 return Ok(Vec::new());
12785 }
12786 self.advance();
12787 if !self.peek_is_by() {
12788 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12789 }
12790 self.advance();
12791 let mut keys = Vec::new();
12792 loop {
12793 // v7.39 (round 691) — save/restore, the discipline this parser
12794 // already uses around `pending_sample_preds`, so a subquery inside
12795 // a key neither inherits nor leaks the channel.
12796 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12797 let saved_coll = self.order_key_collation.take();
12798 let parsed = self.parse_expr(0);
12799 self.in_order_by_key = saved_flag;
12800 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12801 let expr = parsed?;
12802 let desc = if matches!(self.peek(), Token::Desc) {
12803 self.advance();
12804 true
12805 } else if matches!(self.peek(), Token::Asc) {
12806 self.advance();
12807 false
12808 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12809 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12810 // one ordering per type, so the btree comparison operators map
12811 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12812 // would need a custom operator class — honest error.
12813 self.advance();
12814 match self.advance() {
12815 Token::Lt | Token::LtEq => false,
12816 Token::Gt | Token::GtEq => true,
12817 other => {
12818 return Err(self.err(alloc::format!(
12819 "ORDER BY USING supports the btree comparison \
12820 operators (< <= > >=); got {other:?}"
12821 )));
12822 }
12823 }
12824 } else {
12825 false
12826 };
12827 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12828 let nulls_first = self.parse_optional_nulls_placement()?;
12829 keys.push(OrderBy {
12830 expr,
12831 desc,
12832 nulls_first,
12833 collation,
12834 });
12835 if matches!(self.peek(), Token::Comma) {
12836 self.advance();
12837 } else {
12838 break;
12839 }
12840 }
12841 Ok(keys)
12842 }
12843
12844 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12845 // v7.39 (round 135) — a grouping-set query may have already parsed +
12846 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12847 // no ORDER BY token is present, keep that pre-set order_by rather than
12848 // clobbering it with an empty list.
12849 let parsed_keys = self.parse_order_by_keys()?;
12850 head.order_by = if parsed_keys.is_empty() {
12851 core::mem::take(&mut head.order_by)
12852 } else {
12853 parsed_keys
12854 };
12855 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12856 // order. PG's grammar takes a limit clause and an offset clause
12857 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12858 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12859 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12860 // spelling died on `expected end of input, got Limit`.
12861 //
12862 // Each may appear at most once, and LIMIT and FETCH FIRST are
12863 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12864 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12865 // A second one is left unconsumed here, which the caller reports
12866 // as trailing input rather than silently taking the last.
12867 let mut saw_limit = false;
12868 let mut saw_offset = false;
12869 loop {
12870 if !saw_limit && matches!(self.peek(), Token::Limit) {
12871 self.advance();
12872 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12873 // PG synonyms for "no limit". Treat both as None
12874 // (no head.limit set) so the engine's existing
12875 // unlimited-result path takes over. Reject was the
12876 // pre-5.1 behaviour and broke pg_dump-flavoured
12877 // tooling that occasionally emits LIMIT NULL.
12878 if self.consume_limit_unbounded_sentinel() {
12879 head.limit = None;
12880 } else {
12881 let first = self.parse_limit_expr("LIMIT")?;
12882 // MySQL `LIMIT offset, count` — the first number is
12883 // the offset when a comma follows.
12884 if matches!(self.peek(), Token::Comma) {
12885 self.advance();
12886 let count = self.parse_limit_expr("LIMIT")?;
12887 head.offset = Some(first);
12888 saw_offset = true;
12889 head.limit = Some(count);
12890 } else {
12891 head.limit = Some(first);
12892 }
12893 }
12894 saw_limit = true;
12895 continue;
12896 }
12897 if !saw_offset && matches!(self.peek(), Token::Offset) {
12898 self.advance();
12899 // PG also accepts an optional `ROW` / `ROWS` trailer
12900 // after the offset value (`OFFSET 10 ROWS`). The
12901 // FETCH-FIRST branch below relies on the same.
12902 let off = self.parse_limit_expr("OFFSET")?;
12903 self.consume_optional_rows_keyword();
12904 head.offset = Some(off);
12905 saw_offset = true;
12906 continue;
12907 }
12908 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12909 // the SQL-standard alias for LIMIT. PG accepts both
12910 // spellings interchangeably; pg_dump emits FETCH FIRST in
12911 // newer versions. We map it onto `head.limit` so the
12912 // engine path is unified.
12913 if !saw_limit
12914 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12915 if s.eq_ignore_ascii_case("fetch"))
12916 {
12917 self.advance(); // FETCH
12918 // `FIRST` or `NEXT` (both legal per SQL standard).
12919 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12920 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12921 {
12922 self.advance();
12923 }
12924 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12925 // implicit 1 — but we always consume one if present).
12926 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12927 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12928 {
12929 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12930 crate::ast::LimitExpr::Literal(1)
12931 } else {
12932 self.parse_limit_expr("FETCH FIRST")?
12933 };
12934 // Eat `ROW` / `ROWS` if not already consumed above.
12935 self.consume_optional_rows_keyword();
12936 // Optional `ONLY` (the spec form) — or the SQL:2008
12937 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12938 // now honours WITH TIES by extending past the LIMIT
12939 // truncation point through every row that shares the
12940 // last-kept row's ORDER BY key.
12941 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12942 if s.eq_ignore_ascii_case("only"))
12943 {
12944 self.advance();
12945 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12946 if s.eq_ignore_ascii_case("with"))
12947 {
12948 self.advance(); // WITH
12949 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12950 if s.eq_ignore_ascii_case("ties"))
12951 {
12952 self.advance();
12953 head.limit_with_ties = true;
12954 }
12955 }
12956 head.limit = Some(count);
12957 saw_limit = true;
12958 continue;
12959 }
12960 break;
12961 }
12962 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12963 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12964 // [ OF table_name [, …] ]
12965 // [ NOWAIT | SKIP LOCKED ]
12966 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12967 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12968 // SELECT already returns a consistent snapshot — so these
12969 // are accept-and-discard: the parser absorbs them so
12970 // mailrs / Rails / Django code paths that emit `SELECT
12971 // … FOR UPDATE` for advisory pessimistic locking load
12972 // without a parser error. The on-disk locking model is
12973 // unchanged; callers that rely on FOR UPDATE for read-
12974 // through-write ordering still get the right answer
12975 // because SPG serialises writes anyway.
12976 head.locking = self
12977 .consume_optional_for_lock_clauses()
12978 .map(alloc::boxed::Box::new);
12979 Ok(())
12980 }
12981
12982 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12983 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12984 /// LOCKED ]` trailers. Each clause is fully accepted and
12985 /// discarded — SPG's single-writer model already satisfies the
12986 /// callers' implicit ordering requirement. Stops at the first
12987 /// token that isn't `FOR`.
12988 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12989 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12990 // not discarded. PG keeps the strongest of several clauses; the
12991 // policy of the last one wins, which is what this loop records.
12992 let mut seen: Option<crate::ast::LockingClause> = None;
12993 while matches!(self.peek(), Token::For) {
12994 // v7.37.14 (A2.5-stub) — record that this query asked
12995 // for a row lock the parser is about to silently
12996 // discard. Operators surface the count via
12997 // `spg_sql::silent_for_update_count()` so they can
12998 // gauge how much of the workload depends on advisory
12999 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13000 // before v7.37.15's per-row tuple locking lands.
13001 crate::record_silent_for_update_clause();
13002 self.advance(); // FOR
13003 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13004 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13005 let mut no_key = false;
13006 let mut key = false;
13007 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13008 if s.eq_ignore_ascii_case("no"))
13009 {
13010 self.advance(); // NO
13011 no_key = true;
13012 // The next ident should be KEY but be generous;
13013 // anything followed by UPDATE/SHARE is accepted.
13014 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13015 if s.eq_ignore_ascii_case("key"))
13016 {
13017 self.advance(); // KEY
13018 }
13019 }
13020 // `KEY` prefix (PG `FOR KEY SHARE`).
13021 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13022 if s.eq_ignore_ascii_case("key"))
13023 {
13024 self.advance(); // KEY
13025 key = true;
13026 }
13027 // Lock-strength keyword: UPDATE / SHARE. Required, but
13028 // we're lenient — an unexpected token here just bails
13029 // (we already consumed FOR; caller's downstream
13030 // dispatch will error if anything actually depends on
13031 // the trailing tokens).
13032 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13033 if s.eq_ignore_ascii_case("update"));
13034 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13035 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13036 {
13037 self.advance();
13038 use crate::ast::LockStrength as LS;
13039 let strength = match (is_update, no_key, key) {
13040 (true, true, _) => LS::NoKeyUpdate,
13041 (true, _, _) => LS::Update,
13042 (false, _, true) => LS::KeyShare,
13043 (false, _, _) => LS::Share,
13044 };
13045 seen = Some(crate::ast::LockingClause {
13046 strength,
13047 of_tables: alloc::vec::Vec::new(),
13048 policy: crate::ast::LockWait::Wait,
13049 });
13050 } else {
13051 // FOR by itself (or `FOR KEY` with nothing after) —
13052 // give up on the lock-clause path. We've already
13053 // advanced past FOR; further attempts to parse
13054 // here would clobber state.
13055 return seen;
13056 }
13057 // Optional `OF tbl[, tbl …]`. mailrs emits this when
13058 // joining and locking only a subset of tables.
13059 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13060 if s.eq_ignore_ascii_case("of"))
13061 {
13062 self.advance(); // OF
13063 #[allow(clippy::while_let_loop)]
13064 loop {
13065 match self.peek() {
13066 Token::Ident(_) | Token::QuotedIdent(_) => {
13067 // v7.39 (round 294) — the name is CAPTURED now: PG
13068 // validates it against the FROM clause, and an
13069 // uncaptured list silently means "lock everything".
13070 let mut nm = match self.advance() {
13071 Token::Ident(n) | Token::QuotedIdent(n) => n,
13072 _ => alloc::string::String::new(),
13073 };
13074 // Optional schema-qualified `schema.table`.
13075 if matches!(self.peek(), Token::Dot) {
13076 self.advance();
13077 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13078 {
13079 self.advance();
13080 nm = n;
13081 }
13082 }
13083 if let Some(c) = seen.as_mut() {
13084 c.of_tables.push(nm);
13085 }
13086 }
13087 _ => break,
13088 }
13089 if matches!(self.peek(), Token::Comma) {
13090 self.advance();
13091 } else {
13092 break;
13093 }
13094 }
13095 }
13096 // Optional `NOWAIT` | `SKIP LOCKED`.
13097 match self.peek().clone() {
13098 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13099 self.advance();
13100 if let Some(c) = seen.as_mut() {
13101 c.policy = crate::ast::LockWait::NoWait;
13102 }
13103 }
13104 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13105 self.advance(); // SKIP
13106 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13107 if s.eq_ignore_ascii_case("locked"))
13108 {
13109 self.advance(); // LOCKED
13110 if let Some(c) = seen.as_mut() {
13111 c.policy = crate::ast::LockWait::SkipLocked;
13112 }
13113 }
13114 }
13115 _ => {}
13116 }
13117 // Loop: PG allows multiple FOR clauses chained.
13118 }
13119 seen
13120 }
13121
13122 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13123 /// Bind value gets resolved during prepared-statement Execute;
13124 /// the Pratt expression parser would over-accept here (e.g.
13125 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13126 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13127 /// sentinel tokens (PG synonyms for "no limit"). Returns true
13128 /// when one was consumed; caller skips the regular
13129 /// limit-value parse and leaves `head.limit` at None.
13130 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13131 if matches!(self.peek(), Token::Null) {
13132 self.advance();
13133 return true;
13134 }
13135 if matches!(self.peek(), Token::All) {
13136 self.advance();
13137 return true;
13138 }
13139 false
13140 }
13141
13142 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13143 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13144 /// SQL-standard shape. No-op when missing.
13145 fn consume_optional_rows_keyword(&mut self) {
13146 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13147 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13148 {
13149 self.advance();
13150 }
13151 }
13152
13153 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13154 ///
13155 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13156 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13157 /// constant, which is why that spelling keeps the token path below.
13158 ///
13159 /// Constants are folded here rather than carried into the tree: the
13160 /// 15+ execution paths that read the row count go through
13161 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13162 /// means "no limit". A clause the engine could not resolve would
13163 /// therefore return the WHOLE table instead of failing. Folding at
13164 /// parse time keeps that impossible; a non-constant clause is still
13165 /// a clean error (recorded residual — closing it wants a resolution
13166 /// pre-pass on the simple-query path, where `substitute_placeholders`
13167 /// does not run).
13168 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13169 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13170 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13171 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13172 // ONLY` both work (its grammar takes a c_expr). Both measured
13173 // against PG 18.4 in round 305.
13174 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13175 return self.parse_limit_constant(label);
13176 }
13177 // One pass, no rewind: `advance()` takes each token by
13178 // `mem::replace`, so a consumed token reads back as Eof and this
13179 // parser cannot backtrack. Everything — bare literal included —
13180 // is therefore folded from the parsed expression rather than
13181 // re-read from the token stream.
13182 let start = self.pos;
13183 let e = self.parse_expr(0)?;
13184 if let crate::ast::Expr::Placeholder(n) = e {
13185 return Ok(crate::ast::LimitExpr::Placeholder(n));
13186 }
13187 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13188 match fold_limit_constant(&e) {
13189 Some(Ok(v)) if v < 0 => Err(ParseError {
13190 message: alloc::format!("{neg_label} must not be negative"),
13191 token_pos: start,
13192 }),
13193 Some(Ok(v)) => u32::try_from(v)
13194 .map(crate::ast::LimitExpr::Literal)
13195 .map_err(|_| ParseError {
13196 message: alloc::format!("{label} value too large: {v}"),
13197 token_pos: start,
13198 }),
13199 Some(Err(message)) => Err(ParseError {
13200 message: message.replace("{L}", neg_label),
13201 token_pos: start,
13202 }),
13203 // v7.39 (round 305, V23) — not foldable at parse time
13204 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13205 // expression; the engine evaluates it once before dispatch.
13206 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13207 }
13208 }
13209
13210 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13211 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13212 // coercion rules, not just an integer token: a NUMERIC rounds half
13213 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13214 // refused with PG's wording ("LIMIT must not be negative", 2201W /
13215 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13216 // content, failing as an input-syntax error on the value. General
13217 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13218 // they need an Expr-carrying LimitExpr variant.
13219 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13220 let err_at = |message: alloc::string::String, pos: usize| ParseError {
13221 message,
13222 token_pos: pos,
13223 };
13224 match self.advance() {
13225 Token::Integer(n) if n >= 0 => u32::try_from(n)
13226 .map(crate::ast::LimitExpr::Literal)
13227 .map_err(|_| ParseError {
13228 message: alloc::format!("{label} value too large: {n}"),
13229 token_pos: self.consumed_pos(),
13230 }),
13231 Token::Integer(_) => Err(err_at(
13232 alloc::format!("{neg_label} must not be negative"),
13233 self.pos.saturating_sub(1),
13234 )),
13235 Token::Numeric(t) => {
13236 let pos = self.pos.saturating_sub(1);
13237 let v: f64 = t.parse().map_err(|_| {
13238 err_at(
13239 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13240 pos,
13241 )
13242 })?;
13243 if v < 0.0 {
13244 return Err(err_at(
13245 alloc::format!("{neg_label} must not be negative"),
13246 pos,
13247 ));
13248 }
13249 // Round half away from zero — PG's numeric→bigint cast.
13250 // (no_std: no f64::round; v is non-negative, so truncating
13251 // v + 0.5 is the same thing.)
13252 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13253 let rounded = (v + 0.5) as u64;
13254 u32::try_from(rounded)
13255 .map(crate::ast::LimitExpr::Literal)
13256 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13257 }
13258 Token::Minus => {
13259 let pos = self.pos.saturating_sub(1);
13260 match self.peek() {
13261 Token::Integer(_) | Token::Numeric(_) => {
13262 self.advance();
13263 Err(err_at(
13264 alloc::format!("{neg_label} must not be negative"),
13265 pos,
13266 ))
13267 }
13268 other => Err(err_at(
13269 alloc::format!(
13270 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13271 ),
13272 pos,
13273 )),
13274 }
13275 }
13276 Token::String(t) => {
13277 let pos = self.pos.saturating_sub(1);
13278 match t.trim().parse::<i64>() {
13279 Ok(n) if n < 0 => Err(err_at(
13280 alloc::format!("{neg_label} must not be negative"),
13281 pos,
13282 )),
13283 Ok(n) => u32::try_from(n)
13284 .map(crate::ast::LimitExpr::Literal)
13285 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13286 Err(_) => Err(err_at(
13287 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13288 pos,
13289 )),
13290 }
13291 }
13292 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13293 other => Err(ParseError {
13294 message: alloc::format!(
13295 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13296 ),
13297 token_pos: self.consumed_pos(),
13298 }),
13299 }
13300 }
13301
13302 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13303 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13304 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13305 /// `parse_select_stmt` is responsible for filling those in.
13306 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13307 /// call in the expression tree to the per-set integer bitmask
13308 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13309 /// is dropped in this grouping set). Runs during the ROLLUP /
13310 /// CUBE / GROUPING SETS expansion, where the set is known.
13311 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13312 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13313 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13314 if let Expr::FunctionCall { name, .. } = expr
13315 && name.eq_ignore_ascii_case("grouping")
13316 {
13317 if !out.iter().any(|e| e == expr) {
13318 out.push(expr.clone());
13319 }
13320 return;
13321 }
13322 match expr {
13323 Expr::Binary { lhs, rhs, .. } => {
13324 Self::collect_grouping_calls(lhs, out);
13325 Self::collect_grouping_calls(rhs, out);
13326 }
13327 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13328 Self::collect_grouping_calls(expr, out)
13329 }
13330 Expr::FunctionCall { args, .. } => {
13331 for a in args {
13332 Self::collect_grouping_calls(a, out);
13333 }
13334 }
13335 Expr::Case {
13336 operand,
13337 branches,
13338 else_branch,
13339 } => {
13340 if let Some(o) = operand {
13341 Self::collect_grouping_calls(o, out);
13342 }
13343 for (c, v) in branches {
13344 Self::collect_grouping_calls(c, out);
13345 Self::collect_grouping_calls(v, out);
13346 }
13347 if let Some(x) = else_branch {
13348 Self::collect_grouping_calls(x, out);
13349 }
13350 }
13351 _ => {}
13352 }
13353 }
13354
13355 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13356 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13357 /// `__grp_ord_k` (injected per grouping-set branch).
13358 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13359 if let Expr::FunctionCall { name, .. } = expr
13360 && name.eq_ignore_ascii_case("grouping")
13361 {
13362 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13363 *expr = Expr::Column(crate::ast::ColumnName {
13364 qualifier: None,
13365 name: alloc::format!("__grp_ord_{k}"),
13366 });
13367 }
13368 return;
13369 }
13370 match expr {
13371 Expr::Binary { lhs, rhs, .. } => {
13372 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13373 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13374 }
13375 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13376 Self::rewrite_grouping_to_col(expr, grp_exprs)
13377 }
13378 Expr::FunctionCall { args, .. } => {
13379 for a in args {
13380 Self::rewrite_grouping_to_col(a, grp_exprs);
13381 }
13382 }
13383 Expr::Case {
13384 operand,
13385 branches,
13386 else_branch,
13387 } => {
13388 if let Some(o) = operand {
13389 Self::rewrite_grouping_to_col(o, grp_exprs);
13390 }
13391 for (c, v) in branches {
13392 Self::rewrite_grouping_to_col(c, grp_exprs);
13393 Self::rewrite_grouping_to_col(v, grp_exprs);
13394 }
13395 if let Some(x) = else_branch {
13396 Self::rewrite_grouping_to_col(x, grp_exprs);
13397 }
13398 }
13399 _ => {}
13400 }
13401 }
13402
13403 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13404 /// as the list of key sets it contributes. A bare expression is one
13405 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13406 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13407 /// the concatenation of its items' sets, where an item is itself an
13408 /// element, a parenthesized key list, or the empty set `()`. A
13409 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13410 /// move together.
13411 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13412 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13413 // ROLLUP ( … ) / CUBE ( … )
13414 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13415 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13416 {
13417 let is_cube = is_kw(self.peek(), "cube");
13418 self.advance(); // ROLLUP / CUBE
13419 self.advance(); // (
13420 let mut units: Vec<Vec<Expr>> = Vec::new();
13421 loop {
13422 if matches!(self.peek(), Token::LParen) {
13423 // Composite unit: (a, b) rolls up as one.
13424 self.advance();
13425 let mut unit = Vec::new();
13426 if !matches!(self.peek(), Token::RParen) {
13427 loop {
13428 unit.push(self.parse_expr(0)?);
13429 match self.peek() {
13430 Token::Comma => {
13431 self.advance();
13432 }
13433 Token::RParen => break,
13434 other => {
13435 return Err(self.err(format!(
13436 "expected ',' or ')' in grouping unit, got {other:?}"
13437 )));
13438 }
13439 }
13440 }
13441 }
13442 self.advance(); // )
13443 units.push(unit);
13444 } else {
13445 units.push(alloc::vec![self.parse_expr(0)?]);
13446 }
13447 match self.peek() {
13448 Token::Comma => {
13449 self.advance();
13450 }
13451 Token::RParen => break,
13452 other => {
13453 return Err(self.err(format!(
13454 "expected ',' or ')' in grouping list, got {other:?}"
13455 )));
13456 }
13457 }
13458 }
13459 self.advance(); // )
13460 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13461 units
13462 .iter()
13463 .zip(unit_sel.iter())
13464 .filter(|(_, keep)| **keep)
13465 .flat_map(|(u, _)| u.iter().cloned())
13466 .collect()
13467 };
13468 let n = units.len();
13469 if is_cube {
13470 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13471 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13472 .collect();
13473 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13474 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13475 }
13476 return Ok((0..=n)
13477 .rev()
13478 .map(|keep| {
13479 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13480 flatten(&sel)
13481 })
13482 .collect());
13483 }
13484 // GROUPING SETS ( item [, item]* )
13485 if is_kw(self.peek(), "grouping")
13486 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13487 {
13488 self.advance(); // GROUPING
13489 self.advance(); // SETS
13490 if !matches!(self.peek(), Token::LParen) {
13491 return Err(self.err(format!(
13492 "expected '(' after GROUPING SETS, got {:?}",
13493 self.peek()
13494 )));
13495 }
13496 self.advance(); // outer (
13497 let mut sets: Vec<Vec<Expr>> = Vec::new();
13498 loop {
13499 if matches!(self.peek(), Token::LParen) {
13500 // A parenthesized key list (or the empty set).
13501 self.advance();
13502 let mut set = Vec::new();
13503 if !matches!(self.peek(), Token::RParen) {
13504 loop {
13505 set.push(self.parse_expr(0)?);
13506 match self.peek() {
13507 Token::Comma => {
13508 self.advance();
13509 }
13510 Token::RParen => break,
13511 other => {
13512 return Err(self.err(format!(
13513 "expected ',' or ')' in grouping set, got {other:?}"
13514 )));
13515 }
13516 }
13517 }
13518 }
13519 self.advance(); // )
13520 sets.push(set);
13521 } else {
13522 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13523 // bare expression.
13524 sets.extend(self.parse_grouping_element()?);
13525 }
13526 match self.peek() {
13527 Token::Comma => {
13528 self.advance();
13529 }
13530 Token::RParen => break,
13531 other => {
13532 return Err(self.err(format!(
13533 "expected ',' or ')' after a grouping set, got {other:?}"
13534 )));
13535 }
13536 }
13537 }
13538 self.advance(); // outer )
13539 return Ok(sets);
13540 }
13541 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13542 }
13543
13544 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13545 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13546 // set evaluates to NULL, at any depth. Previously only a *top-level*
13547 // select item equal to a dropped key was nullified, so a key nested in
13548 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13549 // column and failed to resolve against the set's synthetic schema.
13550 if dropped.iter().any(|d| d == expr) {
13551 *expr = Expr::Literal(Literal::Null);
13552 return;
13553 }
13554 if let Expr::FunctionCall { name, args } = expr
13555 && name.eq_ignore_ascii_case("grouping")
13556 {
13557 let mut mask: i64 = 0;
13558 for a in args.iter() {
13559 mask <<= 1;
13560 if dropped.iter().any(|d| d == a) {
13561 mask |= 1;
13562 }
13563 }
13564 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13565 // literal: a bare integer in a select item is indistinguishable
13566 // from a positional reference once `ORDER BY 1` substitutes the
13567 // item back in, and the round-232 position check then read the
13568 // mask value as an out-of-range position. The cast changes
13569 // nothing semantically (grouping() is integer).
13570 *expr = Expr::Cast {
13571 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13572 target: crate::ast::CastTarget::Int,
13573 };
13574 return;
13575 }
13576 // Generic recursion over the common expression shapes the
13577 // SELECT list uses; anything without child expressions is
13578 // left alone.
13579 match expr {
13580 // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13581 //
13582 // A grouping column is NULL in the OUTPUT of a set that
13583 // drops it, and an aggregate over it still aggregates the
13584 // real values. Measured, over 0,1,2,3:
13585 //
13586 // ```text
13587 // SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13588 // PostgreSQL 18.6 the total row is NULL | 6
13589 // MySQL 9.7.2 the total row is NULL | 6
13590 // SPG 7.39.13 NULL | NULL
13591 // ```
13592 //
13593 // The round that made this walk descend "at any depth" was
13594 // right about `COALESCE(g,'TOTAL')` and wrong about
13595 // `SUM(g)`: it turned the aggregate's own input into a NULL
13596 // literal, so the grand total of a rollup keyed on the
13597 // summed column answered nothing. Wrong on BOTH faces.
13598 //
13599 // `grouping(…)` is settled above, before this, so it keeps
13600 // reading the dropped set.
13601 Expr::FunctionCall { name, args } => {
13602 if is_aggregate_function_name(name) {
13603 return;
13604 }
13605 for a in args {
13606 Self::substitute_grouping_calls(a, dropped);
13607 }
13608 }
13609 Expr::AggregateOrdered { .. } => {}
13610 Expr::Binary { lhs, rhs, .. } => {
13611 Self::substitute_grouping_calls(lhs, dropped);
13612 Self::substitute_grouping_calls(rhs, dropped);
13613 }
13614 Expr::Unary { expr: inner, .. } => {
13615 Self::substitute_grouping_calls(inner, dropped);
13616 }
13617 Expr::Cast { expr: inner, .. } => {
13618 Self::substitute_grouping_calls(inner, dropped);
13619 }
13620 Expr::Case {
13621 operand,
13622 branches,
13623 else_branch,
13624 } => {
13625 if let Some(op) = operand {
13626 Self::substitute_grouping_calls(op, dropped);
13627 }
13628 for (w, t) in branches {
13629 Self::substitute_grouping_calls(w, dropped);
13630 Self::substitute_grouping_calls(t, dropped);
13631 }
13632 if let Some(e) = else_branch {
13633 Self::substitute_grouping_calls(e, dropped);
13634 }
13635 }
13636 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13637 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13638 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13639 // …` is the canonical rollup-total label idiom).
13640 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13641 Expr::Like { expr, pattern, .. } => {
13642 Self::substitute_grouping_calls(expr, dropped);
13643 Self::substitute_grouping_calls(pattern, dropped);
13644 }
13645 Expr::InList { expr, list, .. } => {
13646 Self::substitute_grouping_calls(expr, dropped);
13647 for item in list {
13648 Self::substitute_grouping_calls(item, dropped);
13649 }
13650 }
13651 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13652 Expr::Array(items) => {
13653 for item in items {
13654 Self::substitute_grouping_calls(item, dropped);
13655 }
13656 }
13657 Expr::ArraySubscript { target, index } => {
13658 Self::substitute_grouping_calls(target, dropped);
13659 Self::substitute_grouping_calls(index, dropped);
13660 }
13661 Expr::ArraySlice { target, lo, hi } => {
13662 Self::substitute_grouping_calls(target, dropped);
13663 if let Some(lo) = lo {
13664 Self::substitute_grouping_calls(lo, dropped);
13665 }
13666 if let Some(hi) = hi {
13667 Self::substitute_grouping_calls(hi, dropped);
13668 }
13669 }
13670 Expr::AnyAll { expr, array, .. } => {
13671 Self::substitute_grouping_calls(expr, dropped);
13672 Self::substitute_grouping_calls(array, dropped);
13673 }
13674 _ => {}
13675 }
13676 }
13677
13678 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13679 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13680 // group: `( <select chain> )` usable anywhere a query block
13681 // is (head or peer of an outer chain). The group's own
13682 // unions ride the returned SelectStatement; the executor's
13683 // nested-peer recursion runs them.
13684 if matches!(self.peek(), Token::LParen)
13685 && matches!(
13686 self.tokens.get(self.pos + 1),
13687 Some(Token::Select | Token::LParen | Token::Values)
13688 )
13689 {
13690 self.advance(); // (
13691 self.enter_nested()?;
13692 // v7.37 D.20 — a group whose head is a VALUES list:
13693 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13694 // otherwise recurse into a nested SELECT/group head.
13695 let mut head = (if matches!(self.peek(), Token::Values) {
13696 self.advance(); // VALUES
13697 self.parse_values_rows_body()
13698 } else {
13699 self.parse_bare_select()
13700 })
13701 .and_then(|mut h| {
13702 self.parse_setop_chain_into(&mut h)?;
13703 Ok(h)
13704 });
13705 self.nest_depth -= 1;
13706 let mut head = match &mut head {
13707 Ok(h) => core::mem::take(h),
13708 Err(_) => return head,
13709 };
13710 // v7.37.17 (17.6 siblings) — group-internal tail:
13711 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13712 // group head, then wrap the group as a derived table
13713 // (SELECT * FROM (group)) so the outer chain / outer
13714 // tail can't clobber the group's own ordering or limit.
13715 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13716 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13717 if s.eq_ignore_ascii_case("fetch"));
13718 if has_tail {
13719 self.parse_select_tail_into(&mut head)?;
13720 head = SelectStatement {
13721 locking: None,
13722 ctes: Vec::new(),
13723 distinct: false,
13724 distinct_on: Vec::new(),
13725 items: alloc::vec![SelectItem::Wildcard],
13726 from: Some(FromClause {
13727 primary: TableRef {
13728 name: "subquery".to_string(),
13729 alias: None,
13730 only: false,
13731 as_of_segment: None,
13732 unnest_expr: None,
13733 unnest_column_aliases: Vec::new(),
13734 with_ordinality: false,
13735 generate_series_args: None,
13736 lateral_subquery: Some(Box::new(head)),
13737 jsonb_each_text_arg: None,
13738 table_fn_call: None,
13739 rows_from: None,
13740 json_table: None,
13741 scalar_fn_item: false,
13742 },
13743 joins: Vec::new(),
13744 }),
13745 where_: None,
13746 group_by: None,
13747 group_by_all: false,
13748 having: None,
13749 unions: Vec::new(),
13750 order_by: Vec::new(),
13751 limit: None,
13752 offset: None,
13753 limit_with_ties: false,
13754 window_check_exprs: Vec::new(),
13755 };
13756 }
13757 if !matches!(self.peek(), Token::RParen) {
13758 return Err(self.err(format!(
13759 "expected ')' after parenthesized query group, got {:?}",
13760 self.peek()
13761 )));
13762 }
13763 self.advance();
13764 return Ok(head);
13765 }
13766 // `TABLE name` shorthand as a query block — valid anywhere
13767 // a SELECT head is (set-op peers included).
13768 if matches!(self.peek(), Token::Table)
13769 && matches!(
13770 self.tokens.get(self.pos + 1),
13771 Some(Token::Ident(_) | Token::QuotedIdent(_))
13772 )
13773 {
13774 return self.parse_table_shorthand();
13775 }
13776 if !matches!(self.peek(), Token::Select) {
13777 return Err(self.err(format!(
13778 "expected SELECT to start a query block, got {:?}",
13779 self.peek()
13780 )));
13781 }
13782 self.advance();
13783 // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13784 //
13785 // It sits where `DISTINCT` sits and tells the optimiser to join
13786 // in the written order. SPG plans its own joins, so the hint is
13787 // accepted and not acted on — but it has to PARSE, because as a
13788 // bare identifier it became a column: measured on the published
13789 // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13790 // column 'straight_join' in 'field list'` where MySQL 9.7.2
13791 // returns the rows. Only in this position, which is the only one
13792 // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13793 if self.mysql_dialect
13794 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13795 {
13796 self.advance();
13797 }
13798 let distinct = if matches!(self.peek(), Token::Distinct) {
13799 self.advance();
13800 true
13801 } else {
13802 false
13803 };
13804 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13805 // keep the first row (per ORDER BY) of each group the
13806 // expressions define. Django's .distinct('field') shape.
13807 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13808 self.advance(); // ON
13809 if !matches!(self.peek(), Token::LParen) {
13810 return Err(self.err(format!(
13811 "expected '(' after DISTINCT ON, got {:?}",
13812 self.peek()
13813 )));
13814 }
13815 self.advance();
13816 let mut exprs = Vec::new();
13817 loop {
13818 exprs.push(self.parse_expr(0)?);
13819 match self.peek() {
13820 Token::Comma => {
13821 self.advance();
13822 }
13823 Token::RParen => break,
13824 other => {
13825 return Err(self.err(format!(
13826 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13827 )));
13828 }
13829 }
13830 }
13831 self.advance(); // )
13832 exprs
13833 } else {
13834 Vec::new()
13835 };
13836 let mut items = self.parse_select_list()?;
13837 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13838 // of CTAS. It sits exactly here in PG's grammar, right after the
13839 // target list.
13840 //
13841 // A comment in `ast.rs` has said since v7.38 that CTAS and
13842 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13843 // `SELECT i INTO t FROM src` answered `syntax error at or near
13844 // "INTO"`, which the differential found while measuring what
13845 // PostgreSQL tags each of the five materialising forms with. A
13846 // comment describing a capability the code does not have is the
13847 // defect this version has been finding all day, and this is the
13848 // one it found in the parser.
13849 //
13850 // `INTO` is captured rather than consumed here: the name has to
13851 // travel out of a function that returns a `SelectStatement`, and
13852 // the caller lowers the whole thing to the CTAS node.
13853 if matches!(self.peek(), Token::Into) {
13854 self.advance();
13855 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13856 // the target, not part of its name. SPG has one storage
13857 // class, so `UNLOGGED` is accepted and means nothing, which
13858 // is what it already means on `CREATE TABLE`.
13859 let mut temporary = false;
13860 loop {
13861 match self.peek().clone() {
13862 Token::Ident(w) | Token::QuotedIdent(w)
13863 if w.eq_ignore_ascii_case("temp")
13864 || w.eq_ignore_ascii_case("temporary") =>
13865 {
13866 temporary = true;
13867 self.advance();
13868 }
13869 Token::Ident(w) | Token::QuotedIdent(w)
13870 if w.eq_ignore_ascii_case("unlogged") =>
13871 {
13872 self.advance();
13873 }
13874 Token::Table => {
13875 self.advance();
13876 }
13877 _ => break,
13878 }
13879 }
13880 let name = match self.peek().clone() {
13881 Token::Ident(w) | Token::QuotedIdent(w) => {
13882 self.advance();
13883 w
13884 }
13885 other => {
13886 return Err(self.err(alloc::format!(
13887 "expected a table name after SELECT … INTO, got {other:?}"
13888 )));
13889 }
13890 };
13891 self.pending_select_into = Some((name, temporary));
13892 }
13893 // Scope the TABLESAMPLE lowering channel to this SELECT:
13894 // stash whatever an enclosing select accumulated, collect
13895 // our own FROM's predicates, restore after the combine.
13896 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13897 let mut from = if matches!(self.peek(), Token::From) {
13898 self.advance();
13899 Some(self.parse_from_clause()?)
13900 } else {
13901 None
13902 };
13903 // v7.37 D.22 — a set-returning function in the projection with no FROM
13904 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13905 // rows. Move the first SRF projection item to a FROM-position derived
13906 // table and replace it in the projection with a reference to its output
13907 // column; sibling scalar columns repeat per SRF row. PG names the output
13908 // column after the function (or its AS alias). Reuses the FROM-SRF
13909 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13910 // works via the targetlist-SRF path.
13911 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13912 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13913 // is exactly what the function's own row shape already is. Anywhere else
13914 // (per outer row, or beside other items) it would need a real record-typed
13915 // projection, so it says so rather than answering something else.
13916 if let [
13917 SelectItem::Expr {
13918 expr: Expr::FunctionCall { name, args },
13919 ..
13920 },
13921 ] = items.as_slice()
13922 && name == "__record_expand"
13923 {
13924 let Some(Expr::FunctionCall {
13925 name: inner_name,
13926 args: inner_args,
13927 }) = args.first()
13928 else {
13929 return Err(self.err(
13930 "(<expr>).* expands a function's record — it needs a function call".into(),
13931 ));
13932 };
13933 if from.is_some() {
13934 return Err(self.err(
13935 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13936 .into(),
13937 ));
13938 }
13939 let fn_ref = TableRef {
13940 name: inner_name.clone(),
13941 alias: None,
13942 only: false,
13943 as_of_segment: None,
13944 unnest_expr: None,
13945 unnest_column_aliases: Vec::new(),
13946 with_ordinality: false,
13947 generate_series_args: None,
13948 lateral_subquery: None,
13949 jsonb_each_text_arg: None,
13950 table_fn_call: Some(Box::new((
13951 inner_name.to_ascii_lowercase(),
13952 inner_args.clone(),
13953 ))),
13954 rows_from: None,
13955 json_table: None,
13956 scalar_fn_item: false,
13957 };
13958 items = alloc::vec![SelectItem::Wildcard];
13959 from = Some(FromClause {
13960 primary: fn_ref,
13961 joins: Vec::new(),
13962 });
13963 }
13964 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13965 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13966 // record's fields takes the catalog. It becomes a LATERAL of the same
13967 // function plus one item per declared column — the machinery rounds 65
13968 // and 69 already built.
13969 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13970 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13971 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13972 // express, since the lifted one becomes a scan and the other would
13973 // expand per its rows (a cross product, not a zip). So when the
13974 // projection holds more than one top-level function call, the lift steps
13975 // aside and the engine's target-list expansion takes the whole list.
13976 let fn_call_items = items
13977 .iter()
13978 .filter(|it| {
13979 matches!(
13980 it,
13981 SelectItem::Expr {
13982 expr: Expr::FunctionCall { .. },
13983 ..
13984 }
13985 )
13986 })
13987 .count();
13988 if from.is_none() && fn_call_items <= 1 {
13989 let mut found: Option<(usize, TableRef, String)> = None;
13990 for (i, item) in items.iter().enumerate() {
13991 if let SelectItem::Expr {
13992 expr: Expr::FunctionCall { name, args },
13993 alias,
13994 } = item
13995 {
13996 let lname = name.to_ascii_lowercase();
13997 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13998 let (unnest, gs) = match lname.as_str() {
13999 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14000 "generate_series" if (2..=3).contains(&args.len()) => {
14001 (None, Some(args.clone()))
14002 }
14003 // v7.38 (read01) — generate_subscripts(arr, dim) in a
14004 // no-FROM projection yields the 1-based subscripts, i.e.
14005 // generate_series(1, array_length(arr, dim)); an invalid
14006 // dimension makes array_length NULL → 0 rows, as in PG.
14007 "generate_subscripts" if args.len() == 2 => (
14008 None,
14009 Some(alloc::vec![
14010 Expr::Literal(Literal::Integer(1)),
14011 Expr::FunctionCall {
14012 name: "array_length".to_string(),
14013 args: args.clone(),
14014 },
14015 ]),
14016 ),
14017 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14018 // in a no-FROM projection unnest their *_to_array form.
14019 "string_to_table" | "regexp_split_to_table" => {
14020 let array_fn = if lname == "string_to_table" {
14021 "string_to_array"
14022 } else {
14023 "regexp_split_to_array"
14024 };
14025 (
14026 Some(Box::new(Expr::FunctionCall {
14027 name: array_fn.to_string(),
14028 args: args.clone(),
14029 })),
14030 None,
14031 )
14032 }
14033 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14034 // a no-FROM projection expand per element. The scalar form
14035 // returns the elements as a TEXT array, so unnest over the
14036 // same call materialises one row each (same rewrite the
14037 // FROM-clause form uses).
14038 "jsonb_array_elements"
14039 | "json_array_elements"
14040 | "jsonb_array_elements_text"
14041 | "json_array_elements_text"
14042 if args.len() == 1 =>
14043 {
14044 (
14045 Some(Box::new(Expr::FunctionCall {
14046 name: lname.clone(),
14047 args: args.clone(),
14048 })),
14049 None,
14050 )
14051 }
14052 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14053 // in a no-FROM projection expands per match (scalar form
14054 // returns the matches as a TEXT array → unnest).
14055 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14056 Some(Box::new(Expr::FunctionCall {
14057 name: lname.clone(),
14058 args: args.clone(),
14059 })),
14060 None,
14061 ),
14062 _ => continue,
14063 };
14064 found = Some((
14065 i,
14066 TableRef {
14067 name: colname.clone(),
14068 alias: Some(colname.clone()),
14069 only: false,
14070 as_of_segment: None,
14071 unnest_expr: unnest,
14072 unnest_column_aliases: alloc::vec![colname.clone()],
14073 with_ordinality: false,
14074 generate_series_args: gs,
14075 lateral_subquery: None,
14076 jsonb_each_text_arg: None,
14077 table_fn_call: None,
14078 rows_from: None,
14079 json_table: None,
14080 scalar_fn_item: false,
14081 },
14082 colname,
14083 ));
14084 break;
14085 }
14086 }
14087 if let Some((idx, tref, colname)) = found {
14088 from = Some(FromClause {
14089 primary: tref,
14090 joins: Vec::new(),
14091 });
14092 items[idx] = SelectItem::Expr {
14093 expr: Expr::Column(ColumnName {
14094 qualifier: None,
14095 name: colname.clone(),
14096 }),
14097 alias: Some(colname),
14098 };
14099 }
14100 }
14101 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14102 let where_ = if matches!(self.peek(), Token::Where) {
14103 self.advance();
14104 Some(self.parse_expr(0)?)
14105 } else {
14106 None
14107 };
14108 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14109 Some(match acc {
14110 Some(w) => Expr::Binary {
14111 lhs: Box::new(pred),
14112 op: crate::ast::BinOp::And,
14113 rhs: Box::new(w),
14114 },
14115 None => pred,
14116 })
14117 });
14118 self.pending_sample_preds = enclosing_sample_preds;
14119 let mut group_by_all = false;
14120 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14121 // share one expansion: `grouping_sets` lists the key subsets
14122 // (first = primary, assigned to stmt.group_by; the rest
14123 // become UNION ALL peers), `grouping_universe` is the full
14124 // key list used to compute each peer's dropped keys.
14125 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14126 let mut grouping_universe: Vec<Expr> = Vec::new();
14127 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14128 // A BOOL, not the key list: this frame is the statement parser's, and
14129 // round 430 measured that a `Vec` local here is enough on its own to
14130 // tip the 512 KiB nesting guard. The keys are recoverable from
14131 // `grouping_universe`, which a rollup fills with exactly them.
14132 let mut mysql_rollup = false;
14133 let group_by = if matches!(self.peek(), Token::Group) {
14134 self.advance();
14135 if !self.peek_is_by() {
14136 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14137 }
14138 self.advance();
14139 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14140 // every non-aggregate SELECT-list item later.
14141 if matches!(self.peek(), Token::All) {
14142 self.advance();
14143 group_by_all = true;
14144 None
14145 } else {
14146 // v7.39 (round 242) — PG's general grouping-element grammar:
14147 // GROUP BY [DISTINCT] element [, element]*, where an element
14148 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14149 // SETS (…) — mixed freely. Each element yields a list of
14150 // key sets; the query's grouping sets are the CARTESIAN
14151 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14152 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14153 // content. ROLLUP/CUBE members may be composite
14154 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14155 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14156 // parser handled only a lone ROLLUP/CUBE/GS as the whole
14157 // clause.
14158 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14159 self.advance();
14160 true
14161 } else {
14162 false
14163 };
14164 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14165 loop {
14166 element_sets.push(self.parse_grouping_element()?);
14167 if matches!(self.peek(), Token::Comma) {
14168 self.advance();
14169 } else {
14170 break;
14171 }
14172 }
14173 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14174 for el in &element_sets {
14175 let mut next: Vec<Vec<Expr>> = Vec::new();
14176 for base in &total {
14177 for set in el {
14178 let mut merged = base.clone();
14179 for k in set {
14180 if !merged.iter().any(|m| m == k) {
14181 merged.push(k.clone());
14182 }
14183 }
14184 next.push(merged);
14185 }
14186 }
14187 total = next;
14188 }
14189 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14190 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14191 // The keys and the aggregates come out identical; the ROW
14192 // ORDER does not, and that is the part a report depends on.
14193 // MySQL interleaves each group's subtotal right after its
14194 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14195 // where the union-of-grouping-sets expansion emits every
14196 // leaf first and then every subtotal. MariaDB REFUSES an
14197 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14198 // order itself — measured on MariaDB 11 and MySQL 9.7, which
14199 // agree on the order and disagree only on whether ORDER BY
14200 // is allowed (MySQL allows it; SPG allows it too, since
14201 // refusing would break the clients that can write it).
14202 if self.mysql_dialect
14203 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14204 && matches!(
14205 self.tokens.get(self.pos + 1),
14206 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14207 )
14208 {
14209 self.advance(); // WITH
14210 self.advance(); // ROLLUP
14211 let keys = total.into_iter().next().unwrap_or_default();
14212 mysql_rollup = true;
14213 // n+1 prefixes, largest first — the same expansion
14214 // `ROLLUP (…)` produces.
14215 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14216 }
14217 if distinct_sets {
14218 let mut seen: Vec<Vec<String>> = Vec::new();
14219 total.retain(|set| {
14220 let mut key: Vec<String> =
14221 set.iter().map(|e| alloc::format!("{e}")).collect();
14222 key.sort();
14223 if seen.contains(&key) {
14224 false
14225 } else {
14226 seen.push(key);
14227 true
14228 }
14229 });
14230 }
14231 if total.len() > 1 {
14232 let mut universe: Vec<Expr> = Vec::new();
14233 for set in &total {
14234 for k in set {
14235 if !universe.iter().any(|u| u == k) {
14236 universe.push(k.clone());
14237 }
14238 }
14239 }
14240 grouping_universe = universe;
14241 let primary = total[0].clone();
14242 grouping_sets = total;
14243 Some(primary)
14244 } else {
14245 // One set (a plain GROUP BY list, or a single-set
14246 // spelling like GROUPING SETS ((a, b))). An EMPTY
14247 // single set — GROUPING SETS (()) — stays
14248 // `Some(vec![])`: the grand-total group, which must
14249 // run the aggregate path.
14250 Some(total.into_iter().next().unwrap_or_default())
14251 }
14252 }
14253 } else {
14254 None
14255 };
14256 let having = if matches!(self.peek(), Token::Having) {
14257 self.advance();
14258 Some(self.parse_expr(0)?)
14259 } else {
14260 None
14261 };
14262 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14263 // OVER w parsed to a marker above; inline each definition
14264 // into the referencing WindowFunction nodes.
14265 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14266 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14267 self.advance();
14268 loop {
14269 let wname = self.expect_ident_like()?;
14270 if !matches!(self.peek(), Token::As) {
14271 return Err(self.err(format!(
14272 "expected AS after WINDOW {wname}, got {:?}",
14273 self.peek()
14274 )));
14275 }
14276 self.advance();
14277 // v7.39 (round 229) — PG rejects a redefinition outright.
14278 if window_defs
14279 .iter()
14280 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14281 {
14282 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14283 }
14284 let def = self.parse_over_clause()?;
14285 // A definition may itself copy an earlier one
14286 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14287 // so resolve it against the defs already in scope. Same
14288 // copy rules as an `OVER (w1 …)` in the select list.
14289 let mut probe = Expr::WindowFunction {
14290 name: String::new(),
14291 args: Vec::new(),
14292 partition_by: def.0,
14293 order_by: def.1,
14294 frame: def.2,
14295 null_treatment: crate::ast::NullTreatment::Respect,
14296 filter: None,
14297 };
14298 Self::substitute_named_windows(&mut probe, &window_defs)
14299 .map_err(|m| self.err(m))?;
14300 let Expr::WindowFunction {
14301 partition_by,
14302 order_by,
14303 frame,
14304 ..
14305 } = probe
14306 else {
14307 unreachable!("probe is a WindowFunction")
14308 };
14309 window_defs.push((wname, (partition_by, order_by, frame)));
14310 if matches!(self.peek(), Token::Comma) {
14311 self.advance();
14312 continue;
14313 }
14314 break;
14315 }
14316 }
14317 // v7.39 (round 705) — which definitions did anything reference?
14318 // The ones nothing did used to be dropped here, unexamined, so
14319 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14320 // definition whether referenced or not. Their key expressions ride
14321 // out on the statement for the engine to resolve.
14322 let mut window_refs: Vec<String> = Vec::new();
14323 if !window_defs.is_empty() {
14324 for it in &items {
14325 if let SelectItem::Expr { expr, .. } = it {
14326 Self::collect_named_window_refs(expr, &mut window_refs);
14327 }
14328 }
14329 }
14330 let window_check_exprs: Vec<Expr> = window_defs
14331 .iter()
14332 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14333 .flat_map(|(_, (partition, order, _))| {
14334 partition
14335 .iter()
14336 .cloned()
14337 .chain(order.iter().map(|(e, _, _)| e.clone()))
14338 })
14339 .collect();
14340 if !window_defs.is_empty()
14341 || items
14342 .iter()
14343 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14344 {
14345 for it in &mut items {
14346 if let SelectItem::Expr { expr, .. } = it {
14347 Self::substitute_named_windows(expr, &window_defs)
14348 .map_err(|m| self.err(m))?;
14349 }
14350 }
14351 }
14352 // `GROUP BY 1` — positional keys substitute with the Nth
14353 // select item's expression (same contract ORDER BY has had
14354 // since v6.x). Out-of-range positions error.
14355 let group_by = match group_by {
14356 Some(mut keys) => {
14357 for k in &mut keys {
14358 if let Expr::Literal(Literal::Integer(n)) = k {
14359 let idx = *n;
14360 if idx < 1 || idx as usize > items.len() {
14361 return Err(self.err(alloc::format!(
14362 "GROUP BY position {idx} is not in select list"
14363 )));
14364 }
14365 match &items[(idx - 1) as usize] {
14366 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14367 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14368 return Err(self.err(alloc::format!(
14369 "GROUP BY position {idx} references a wildcard item"
14370 )));
14371 }
14372 }
14373 }
14374 }
14375 Some(keys)
14376 }
14377 None => None,
14378 };
14379 let mut stmt = SelectStatement {
14380 locking: None,
14381 ctes: Vec::new(),
14382 distinct,
14383 distinct_on,
14384 items,
14385 from,
14386 where_,
14387 group_by,
14388 group_by_all,
14389 having,
14390 unions: Vec::new(),
14391 order_by: Vec::new(),
14392 limit: None,
14393 offset: None,
14394 limit_with_ties: false,
14395 window_check_exprs,
14396 };
14397 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14398 // first set is the primary (already on stmt.group_by); each
14399 // further set becomes a UNION ALL peer with its dropped
14400 // keys (universe minus the set) replaced by NULL literals
14401 // in the peer's items and group_by. PG-legal: non-grouped
14402 // select items must be group keys or aggregates, so a
14403 // dropped key's occurrences in the projection are exactly
14404 // the ones to nullify.
14405 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14406 // over a plain GROUP BY (every argument must be a group key; the
14407 // mask is then 0) and rejects anything else with 42803. SPG's
14408 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14409 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14410 // function `grouping`".
14411 if grouping_sets.len() <= 1 {
14412 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14413 let mut calls: Vec<Expr> = Vec::new();
14414 for item in &stmt.items {
14415 if let SelectItem::Expr { expr, .. } = item {
14416 Self::collect_grouping_calls(expr, &mut calls);
14417 }
14418 }
14419 if let Some(h) = &stmt.having {
14420 Self::collect_grouping_calls(h, &mut calls);
14421 }
14422 for call in &calls {
14423 let Expr::FunctionCall { args, .. } = call else {
14424 continue;
14425 };
14426 for a in args {
14427 if !keys.iter().any(|k| k == a) {
14428 return Err(self.err(
14429 "arguments to GROUPING must be grouping expressions of the associated query level"
14430 .to_string(),
14431 ));
14432 }
14433 }
14434 }
14435 if !calls.is_empty() {
14436 for item in &mut stmt.items {
14437 if let SelectItem::Expr { expr, .. } = item {
14438 Self::substitute_grouping_calls(expr, &[]);
14439 }
14440 }
14441 if let Some(h) = &mut stmt.having {
14442 Self::substitute_grouping_calls(h, &[]);
14443 }
14444 }
14445 }
14446 if grouping_sets.len() > 1 {
14447 // The primary set's own dropped keys nullify in the
14448 // HEAD's projection too (GROUPING SETS's first set may
14449 // omit keys other sets use).
14450 let primary = grouping_sets[0].clone();
14451 let head_dropped: Vec<Expr> = grouping_universe
14452 .iter()
14453 .filter(|u| !primary.iter().any(|k| k == *u))
14454 .cloned()
14455 .collect();
14456 for set in grouping_sets.iter().skip(1) {
14457 let mut peer = stmt.clone();
14458 peer.unions = Vec::new();
14459 let dropped: Vec<&Expr> = grouping_universe
14460 .iter()
14461 .filter(|u| !set.iter().any(|k| k == *u))
14462 .collect();
14463 // Empty set = grand-total group: `Some(vec![])` forces
14464 // the aggregate path (one collapsed row) instead of a
14465 // per-row passthrough. See the primary-set note above.
14466 peer.group_by = Some(set.clone());
14467 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14468 for item in &mut peer.items {
14469 if let SelectItem::Expr { expr, alias } = item {
14470 if dropped.iter().any(|d| *d == expr) {
14471 // v7.39 — keep the dropped key's name on the
14472 // NULL literal so the UNION output column
14473 // (and any top-level ORDER BY on it) still
14474 // resolves.
14475 if alias.is_none()
14476 && let Expr::Column(c) = &expr
14477 {
14478 *alias = Some(c.name.clone());
14479 }
14480 *expr = Expr::Literal(Literal::Null);
14481 } else {
14482 Self::substitute_grouping_calls(expr, &dropped_owned);
14483 }
14484 }
14485 }
14486 if let Some(h) = &mut peer.having {
14487 Self::substitute_grouping_calls(h, &dropped_owned);
14488 }
14489 stmt.unions.push((UnionKind::All, peer));
14490 }
14491 for item in &mut stmt.items {
14492 if let SelectItem::Expr { expr, alias } = item {
14493 if head_dropped.iter().any(|d| d == expr) {
14494 if alias.is_none()
14495 && let Expr::Column(c) = &expr
14496 {
14497 *alias = Some(c.name.clone());
14498 }
14499 *expr = Expr::Literal(Literal::Null);
14500 } else {
14501 Self::substitute_grouping_calls(expr, &head_dropped);
14502 }
14503 }
14504 }
14505 if let Some(h) = &mut stmt.having {
14506 Self::substitute_grouping_calls(h, &head_dropped);
14507 }
14508 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14509 // (while `grouping_universe` / the per-branch sets are in scope). For
14510 // each grouping() call in it, inject a per-branch hidden column
14511 // `__grp_ord_K` carrying that branch's mask into the head + every
14512 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14513 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14514 // from the final output. A standalone grouping-set query has ORDER BY
14515 // (not an explicit set-op) next, so consuming it here is safe.
14516 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14517 // rollup carries the hierarchical order: sort by the grouping
14518 // keys with the rolled-up NULLs last, which is exactly the
14519 // interleaving both oracles emit. A client's own ORDER BY wins,
14520 // which is what MySQL does (MariaDB refuses to let one be
14521 // written at all).
14522 // The synthesised keys have to travel the SAME path a written
14523 // ORDER BY does: the block below is what turns a `grouping()`
14524 // call into the per-branch `__grp_ord_K` column the engine can
14525 // actually sort on. Bypassing it left a bare `grouping(text)`
14526 // for the evaluator to reject.
14527 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14528 self.parse_order_by_keys()?
14529 } else if mysql_rollup {
14530 Self::mysql_rollup_order(&grouping_universe)
14531 } else {
14532 Vec::new()
14533 };
14534 if !synthesised_or_parsed.is_empty() {
14535 let mut order_keys = synthesised_or_parsed;
14536 let mut grp_exprs: Vec<Expr> = Vec::new();
14537 for ob in &order_keys {
14538 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14539 }
14540 for (k, gexpr) in grp_exprs.iter().enumerate() {
14541 let colname = alloc::format!("__grp_ord_{k}");
14542 // Head branch (primary set) uses `head_dropped`.
14543 let mut he = gexpr.clone();
14544 Self::substitute_grouping_calls(&mut he, &head_dropped);
14545 stmt.items.push(SelectItem::Expr {
14546 expr: he,
14547 alias: Some(colname.clone()),
14548 });
14549 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14550 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14551 let set = &grouping_sets[i + 1];
14552 let dropped: Vec<Expr> = grouping_universe
14553 .iter()
14554 .filter(|u| !set.iter().any(|k| k == *u))
14555 .cloned()
14556 .collect();
14557 let mut pe = gexpr.clone();
14558 Self::substitute_grouping_calls(&mut pe, &dropped);
14559 peer.items.push(SelectItem::Expr {
14560 expr: pe,
14561 alias: Some(colname.clone()),
14562 });
14563 }
14564 }
14565 for ob in &mut order_keys {
14566 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14567 }
14568 // v7.40.0 — and a KEY the order sorts on that the query
14569 // did not project.
14570 //
14571 // A UNION's ORDER BY can only name output columns, so
14572 // the rollup order synthesised over `grouping_universe`
14573 // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14574 // ROLLUP` and the query answered `column "qty" does not
14575 // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14576 // by the key whether or not it is selected. The key
14577 // travels as a hidden column, exactly as the grouping
14578 // mask above does, and is stripped from the output by
14579 // the same rule.
14580 let mut key_exprs: Vec<Expr> = Vec::new();
14581 for ob in &order_keys {
14582 let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14583 let projected = stmt
14584 .items
14585 .iter()
14586 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14587 if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14588 key_exprs.push(ob.expr.clone());
14589 }
14590 }
14591 for (k, kexpr) in key_exprs.iter().enumerate() {
14592 let colname = alloc::format!("__grp_key_{k}");
14593 let mut he = kexpr.clone();
14594 Self::substitute_grouping_calls(&mut he, &head_dropped);
14595 stmt.items.push(SelectItem::Expr {
14596 expr: he,
14597 alias: Some(colname.clone()),
14598 });
14599 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14600 let set = &grouping_sets[i + 1];
14601 let dropped: Vec<Expr> = grouping_universe
14602 .iter()
14603 .filter(|u| !set.iter().any(|kk| kk == *u))
14604 .cloned()
14605 .collect();
14606 let mut pe = kexpr.clone();
14607 Self::substitute_grouping_calls(&mut pe, &dropped);
14608 peer.items.push(SelectItem::Expr {
14609 expr: pe,
14610 alias: Some(colname.clone()),
14611 });
14612 }
14613 for ob in &mut order_keys {
14614 if &ob.expr == kexpr {
14615 ob.expr = Expr::Column(crate::ast::ColumnName {
14616 name: colname.clone(),
14617 qualifier: None,
14618 });
14619 }
14620 }
14621 }
14622 stmt.order_by = order_keys;
14623 }
14624 }
14625 Ok(stmt)
14626 }
14627
14628 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14629 /// as ORDER BY keys.
14630 ///
14631 /// Per key: the rollup marker, then the key. Sorting on the key alone
14632 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14633 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14634 /// the ROLLUP-introduced NULL last, and both print as NULL.
14635 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14636 /// real group including the data-NULL one, 1 only for the row the
14637 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14638 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14639 ///
14640 /// `#[inline(never)]`: its locals must not join the statement parser's
14641 /// frame, which round 430 measured sitting against the nesting guard.
14642 #[inline(never)]
14643 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14644 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14645 for e in keys {
14646 out.push(OrderBy {
14647 expr: Expr::FunctionCall {
14648 name: "grouping".into(),
14649 args: alloc::vec![e.clone()],
14650 },
14651 desc: false,
14652 nulls_first: None,
14653 collation: None,
14654 });
14655 out.push(OrderBy {
14656 expr: e.clone(),
14657 desc: false,
14658 // MySQL orders NULL first on an ascending key.
14659 nulls_first: Some(true),
14660 collation: None,
14661 });
14662 }
14663 out
14664 }
14665
14666 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14667 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14668 #[inline(never)]
14669 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14670 use crate::ast::MaintainKind;
14671 self.skip_paren_option_list();
14672 let kind = match self.peek() {
14673 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14674 Token::Table | Token::Index => {
14675 self.advance();
14676 MaintainKind::ReindexRelation
14677 }
14678 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14679 "index" | "table" => {
14680 self.advance();
14681 MaintainKind::ReindexRelation
14682 }
14683 "schema" => {
14684 self.advance();
14685 MaintainKind::ReindexSchema
14686 }
14687 "system" | "database" => {
14688 self.advance();
14689 MaintainKind::Whole
14690 }
14691 // PG requires the object type; anything else is the
14692 // caller's problem, not something to swallow.
14693 _ => MaintainKind::ReindexRelation,
14694 },
14695 _ => MaintainKind::Whole,
14696 };
14697 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14698 // allows the plain form, so the modifier is recorded rather than
14699 // skipped. It still has no effect on how the reindex runs.
14700 let mut concurrently = false;
14701 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14702 self.advance();
14703 concurrently = true;
14704 }
14705 let target = self.take_optional_maintain_name();
14706 self.consume_until_statement_boundary();
14707 Ok(Statement::Maintain {
14708 kind,
14709 concurrently,
14710 target,
14711 })
14712 }
14713
14714 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14715 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14716 #[inline(never)]
14717 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14718 use crate::ast::MaintainKind;
14719 self.skip_paren_option_list();
14720 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14721 self.advance();
14722 }
14723 let target = self.take_optional_maintain_name();
14724 self.consume_until_statement_boundary();
14725 Ok(Statement::Maintain {
14726 kind: if target.is_some() {
14727 MaintainKind::ClusterRelation
14728 } else {
14729 MaintainKind::Whole
14730 },
14731 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14732 // transaction block quite happily (measured).
14733 concurrently: false,
14734 target,
14735 })
14736 }
14737
14738 /// The next token as a relation / schema name, when there is one.
14739 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14740 match self.peek() {
14741 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14742 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14743 _ => None,
14744 },
14745 _ => None,
14746 }
14747 }
14748
14749 /// A parenthesised option list, absorbed.
14750 fn skip_paren_option_list(&mut self) {
14751 if !matches!(self.peek(), Token::LParen) {
14752 return;
14753 }
14754 let mut depth = 0usize;
14755 loop {
14756 match self.advance() {
14757 Token::LParen => depth += 1,
14758 Token::RParen => {
14759 depth -= 1;
14760 if depth == 0 {
14761 return;
14762 }
14763 }
14764 Token::Eof => return,
14765 _ => {}
14766 }
14767 }
14768 }
14769
14770 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14771 /// column list.
14772 ///
14773 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14774 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14775 /// / ALL. The three that describe physical storage have no meaning
14776 /// here, so they parse and change nothing rather than making a
14777 /// dump that mentions them fail to load.
14778 ///
14779 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14780 /// parse chain the nesting sentinel is tuned against.
14781 #[inline(never)]
14782 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14783 self.advance(); // LIKE
14784 let source = self.expect_ident_like()?;
14785 let mut options = crate::ast::LikeOptions::default();
14786 loop {
14787 let including = match self.peek() {
14788 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14789 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14790 _ => break,
14791 };
14792 self.advance();
14793 // `ALL` lexes as its own keyword, not an identifier.
14794 let opt = if matches!(self.peek(), Token::All) {
14795 self.advance();
14796 alloc::string::String::from("all")
14797 } else {
14798 self.expect_ident_like()?
14799 };
14800 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14801 o.defaults = on;
14802 o.constraints = on;
14803 o.identity = on;
14804 o.generated = on;
14805 o.indexes = on;
14806 o.comments = on;
14807 };
14808 match opt.to_ascii_lowercase().as_str() {
14809 "all" => set(&mut options, including),
14810 "defaults" => options.defaults = including,
14811 "constraints" => options.constraints = including,
14812 "identity" => options.identity = including,
14813 "generated" => options.generated = including,
14814 "indexes" => options.indexes = including,
14815 "comments" => options.comments = including,
14816 // No storage model to copy into.
14817 "storage" | "statistics" | "compression" => {}
14818 other => {
14819 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14820 }
14821 }
14822 }
14823 Ok(crate::ast::LikeSpec {
14824 source,
14825 at,
14826 options,
14827 keep_index_names: false,
14828 })
14829 }
14830
14831 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14832 // Caller already consumed CREATE; we're sitting on TABLE.
14833 debug_assert!(matches!(self.peek(), Token::Table));
14834 self.advance();
14835 let if_not_exists = self.consume_if_not_exists();
14836 let name = self.expect_ident_like()?;
14837 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14838 // child shape has no column list; the child inherits its
14839 // columns from the parent at engine-DDL time. Detect it
14840 // before the `(` requirement below.
14841 if matches!(self.peek(), Token::Partition)
14842 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14843 {
14844 self.advance(); // PARTITION
14845 self.advance(); // of
14846 let partition_of = self.parse_partition_of_tail()?;
14847 return Ok(Statement::CreateTable(CreateTableStatement {
14848 temporary: false,
14849 name,
14850 engine: None,
14851 auto_increment: None,
14852 columns: Vec::new(),
14853 like_specs: Vec::new(),
14854 inherits: Vec::new(),
14855 if_not_exists,
14856 foreign_keys: Vec::new(),
14857 table_constraints: Vec::new(),
14858 partition_by: None,
14859 partition_of: Some(partition_of),
14860 }));
14861 }
14862 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14863 // the materialized-view materialisation path (run the SELECT, infer the
14864 // column types, create + populate the table) but marks the node so the
14865 // executor creates a plain table without a mat-view registry entry.
14866 if matches!(self.peek(), Token::As) {
14867 self.advance();
14868 let body_stmt = self.parse_select_stmt()?;
14869 let Statement::Select(body) = body_stmt else {
14870 return Err(self.err(format!(
14871 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14872 )));
14873 };
14874 let with_data = self.parse_optional_with_data(true)?;
14875 return Ok(Statement::CreateMaterializedView(
14876 crate::ast::CreateMaterializedViewStatement {
14877 temporary: false,
14878 name,
14879 if_not_exists,
14880 columns: Vec::new(),
14881 body,
14882 with_data,
14883 as_plain_table: true,
14884 },
14885 ));
14886 }
14887 // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14888 // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14889 // written without the parentheses. It was a syntax error, so a
14890 // schema written against MySQL could not be loaded at all.
14891 //
14892 // Measured on MySQL 9.7.2: the copy takes the columns, their
14893 // defaults and the indexes, and takes neither the rows nor the
14894 // foreign keys — which is exactly `INCLUDING ALL` here, since
14895 // SPG's LIKE has never copied foreign keys.
14896 if matches!(self.peek(), Token::Like) {
14897 let at = self.pos;
14898 let spec = self.parse_create_table_like(at)?;
14899 let spec = crate::ast::LikeSpec {
14900 options: crate::ast::LikeOptions {
14901 defaults: true,
14902 constraints: true,
14903 identity: true,
14904 generated: true,
14905 indexes: true,
14906 comments: true,
14907 },
14908 keep_index_names: true,
14909 ..spec
14910 };
14911 return Ok(Statement::CreateTable(CreateTableStatement {
14912 temporary: false,
14913 name,
14914 engine: None,
14915 auto_increment: None,
14916 columns: Vec::new(),
14917 like_specs: alloc::vec![spec],
14918 inherits: Vec::new(),
14919 if_not_exists,
14920 foreign_keys: Vec::new(),
14921 table_constraints: Vec::new(),
14922 partition_by: None,
14923 partition_of: None,
14924 }));
14925 }
14926 if !matches!(self.peek(), Token::LParen) {
14927 return Err(self.err(format!(
14928 "expected '(' after table name, got {:?}",
14929 self.peek()
14930 )));
14931 }
14932 self.advance();
14933 let mut columns = Vec::new();
14934 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14935 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14936 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14937 loop {
14938 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14939 // column list. It is how a child that adds nothing of its own is
14940 // written, and this loop demanded at least one entry: `syntax
14941 // error at or near ")"`. The child takes the parent's columns,
14942 // which the INHERITS clause already arranges.
14943 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14944 self.advance();
14945 break;
14946 }
14947 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14948 // clauses from column definitions. Constraints start
14949 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14950 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14951 // a column.
14952 if self.peek_table_level_pk_start() {
14953 table_constraints.push(self.parse_table_level_primary_key()?);
14954 } else if matches!(self.peek(), Token::Like) {
14955 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14956 // <opt> ]*`. The source table's shape lives in the catalog,
14957 // so this records the clause and the engine expands it.
14958 like_specs.push(self.parse_create_table_like(columns.len())?);
14959 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14960 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14961 table_constraints.push(self.parse_table_level_exclude()?);
14962 } else if self.peek_table_level_unique_start() {
14963 table_constraints.push(self.parse_table_level_unique()?);
14964 } else if self.peek_table_level_check_start() {
14965 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14966 table_constraints.push(self.parse_table_level_check()?);
14967 } else if self.peek_mysql_inline_key_start() {
14968 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14969 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14970 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14971 // inside the column list. Skip name + paren list;
14972 // for UNIQUE KEY, register as a UC.
14973 if let Some(uc) = self.parse_mysql_inline_key()? {
14974 table_constraints.push(uc);
14975 }
14976 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14977 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14978 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14979 // CHECK is named, and the named-CONSTRAINT arm used
14980 // to accept FOREIGN KEY only. The name is accepted
14981 // and discarded — same handling as every other SPG
14982 // constraint name.
14983 self.advance(); // CONSTRAINT
14984 // v7.39 (read01 round 48) — the name is kept now: the schema
14985 // stores it, so DROP / RENAME CONSTRAINT can find it.
14986 let con_name = self.expect_ident_like()?;
14987 let mut tc = match kind {
14988 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14989 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14990 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14991 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14992 };
14993 match &mut tc {
14994 crate::ast::TableConstraint::Check { name, .. }
14995 | crate::ast::TableConstraint::Unique { name, .. }
14996 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14997 | crate::ast::TableConstraint::Exclude { name, .. } => {
14998 *name = Some(con_name);
14999 }
15000 _ => {}
15001 }
15002 table_constraints.push(tc);
15003 } else if self.peek_constraint_or_fk_start() {
15004 foreign_keys.push(self.parse_table_level_fk()?);
15005 } else {
15006 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15007 // v7.13.0 — fold inline UNIQUE / CHECK column
15008 // constraints into table-level entries so the
15009 // engine path stays uniform.
15010 if col.is_unique {
15011 table_constraints.push(crate::ast::TableConstraint::Unique {
15012 name: None,
15013 columns: alloc::vec![col.name.clone()],
15014 nulls_not_distinct: col.unique_nulls_not_distinct,
15015 deferrable: col.constraint_deferrable,
15016 initially_deferred: col.constraint_initially_deferred,
15017 prefix_lengths: Vec::new(),
15018 });
15019 }
15020 if let Some(check_expr) = col.check.clone() {
15021 table_constraints.push(crate::ast::TableConstraint::Check {
15022 name: None,
15023 expr: check_expr,
15024 not_valid: false,
15025 });
15026 }
15027 columns.push(col);
15028 if let Some(fk) = col_level_fk {
15029 foreign_keys.push(fk);
15030 }
15031 }
15032 match self.peek() {
15033 Token::Comma => {
15034 self.advance();
15035 }
15036 Token::RParen => {
15037 self.advance();
15038 break;
15039 }
15040 other => {
15041 return Err(
15042 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15043 );
15044 }
15045 }
15046 }
15047 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15048 // `CREATE TABLE k (LIKE t)` is a complete definition even though
15049 // nothing is written between the parentheses.
15050 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15051 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15052 // empty parentheses were a parse error in their own right — quite apart
15053 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15054 // SPG does not have (filed separately).
15055 let _ = &like_specs;
15056 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15057 // It sits between the column list and the MySQL table options,
15058 // and it was a syntax error until this round.
15059 let mut inherits: Vec<String> = Vec::new();
15060 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15061 if k.eq_ignore_ascii_case("inherits"))
15062 {
15063 self.advance();
15064 if !matches!(self.peek(), Token::LParen) {
15065 return Err(self.err(alloc::format!(
15066 "expected ( after INHERITS, got {:?}",
15067 self.peek()
15068 )));
15069 }
15070 self.advance();
15071 loop {
15072 inherits.push(self.expect_ident_like()?);
15073 if matches!(self.peek(), Token::Comma) {
15074 self.advance();
15075 continue;
15076 }
15077 break;
15078 }
15079 if !matches!(self.peek(), Token::RParen) {
15080 return Err(self.err(alloc::format!(
15081 "expected ) closing INHERITS, got {:?}",
15082 self.peek()
15083 )));
15084 }
15085 self.advance();
15086 }
15087 // v7.14.0 — consume MySQL/MariaDB table options after the
15088 // closing `)`. mysqldump emits things like
15089 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15090 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15091 // SPG accepts all forms as no-ops (each option is
15092 // `<ident> [=] <ident-or-string>` separated by whitespace).
15093 let (engine, auto_increment) = self.consume_mysql_table_options();
15094 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15095 // SPG has no per-table reloptions, so accept and ignore them so a
15096 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15097 self.consume_with_reloptions();
15098 // v7.37.6-B — declarative-partition-parent suffix
15099 // (`PARTITION BY RANGE (key_col)`) sits after the column
15100 // list + MySQL table-options. v7.37.6-B only accepts RANGE
15101 // and locks the key column at one ident; the engine then
15102 // verifies the column type is TIMESTAMPTZ.
15103 let partition_by = if matches!(self.peek(), Token::Partition) {
15104 self.advance(); // PARTITION
15105 if !self.peek_is_by() {
15106 return Err(self.err(format!(
15107 "expected BY after PARTITION, got {:?}",
15108 self.peek()
15109 )));
15110 }
15111 self.advance();
15112 Some(self.parse_partition_by_tail()?)
15113 } else {
15114 None
15115 };
15116 Ok(Statement::CreateTable(CreateTableStatement {
15117 temporary: false,
15118 name,
15119 engine,
15120 auto_increment,
15121 columns,
15122 like_specs,
15123 inherits,
15124 if_not_exists,
15125 foreign_keys,
15126 table_constraints,
15127 partition_by,
15128 partition_of: None,
15129 }))
15130 }
15131
15132 /// v7.37.6-B — case-insensitive ident match helper for the
15133 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15134 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15135 /// didn't burn a global keyword slot for each (see the
15136 /// `Token::Partition` doc-comment in `lexer.rs`).
15137 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15138 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15139 }
15140
15141 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15142 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15143 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15144 use crate::ast::{PartitionBySpec, PartitionKindAst};
15145 let kind = match self.peek() {
15146 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15147 self.advance();
15148 PartitionKindAst::Range
15149 }
15150 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15151 self.advance();
15152 PartitionKindAst::List
15153 }
15154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15155 self.advance();
15156 PartitionKindAst::Hash
15157 }
15158 other => {
15159 return Err(self.err(format!(
15160 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15161 )));
15162 }
15163 };
15164 if !matches!(self.peek(), Token::LParen) {
15165 return Err(self.err(format!(
15166 "expected '(' after PARTITION BY <strategy>, got {:?}",
15167 self.peek()
15168 )));
15169 }
15170 self.advance();
15171 let mut key_columns = Vec::new();
15172 loop {
15173 key_columns.push(self.expect_ident_like()?);
15174 match self.peek() {
15175 Token::Comma => {
15176 self.advance();
15177 }
15178 Token::RParen => {
15179 self.advance();
15180 break;
15181 }
15182 other => {
15183 return Err(self.err(format!(
15184 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15185 )));
15186 }
15187 }
15188 }
15189 if key_columns.is_empty() {
15190 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15191 }
15192 Ok(PartitionBySpec { kind, key_columns })
15193 }
15194
15195 /// v7.37.6-B — after `PARTITION OF`, expect
15196 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15197 /// or
15198 /// <parent> DEFAULT
15199 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15200 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15201 let parent_name = self.expect_ident_like()?;
15202 // v7.37.6-B rejects an explicit column list — the child
15203 // inherits from the parent. mailrs round-7 taught us that
15204 // CREATE TABLE-side schema reconciliation hides drift, so
15205 // we surface this as a parse error rather than silently
15206 // ignoring user columns.
15207 if matches!(self.peek(), Token::LParen) {
15208 return Err(self.err(
15209 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15210 at v7.37.6-B; the child inherits its columns from the parent"
15211 .to_string(),
15212 ));
15213 }
15214 let bounds = match self.peek() {
15215 Token::Default => {
15216 self.advance();
15217 PartitionOfBoundsAst::Default
15218 }
15219 Token::For => {
15220 self.advance();
15221 if !matches!(self.peek(), Token::Values) {
15222 return Err(
15223 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15224 );
15225 }
15226 self.advance();
15227 // WITH is not a reserved Token in the lexer — it lexes
15228 // as Token::Ident("with"). Disambiguate manually.
15229 let want_with = matches!(
15230 self.peek(),
15231 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15232 );
15233 if want_with {
15234 self.advance();
15235 if !matches!(self.peek(), Token::LParen) {
15236 return Err(self.err(format!(
15237 "expected '(' after FOR VALUES WITH, got {:?}",
15238 self.peek()
15239 )));
15240 }
15241 self.advance();
15242 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15243 loop {
15244 let key = self.expect_ident_like()?;
15245 let n = match self.peek().clone() {
15246 Token::Integer(v) if u32::try_from(v).is_ok() => {
15247 self.advance();
15248 v as u32
15249 }
15250 other => {
15251 return Err(self.err(format!(
15252 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15253 )));
15254 }
15255 };
15256 match key.to_ascii_uppercase().as_str() {
15257 "MODULUS" => modulus = Some(n),
15258 "REMAINDER" => remainder = Some(n),
15259 other => {
15260 return Err(self.err(format!(
15261 "FOR VALUES WITH: unknown key {other:?}; \
15262 expected MODULUS or REMAINDER"
15263 )));
15264 }
15265 }
15266 match self.peek() {
15267 Token::Comma => {
15268 self.advance();
15269 }
15270 Token::RParen => {
15271 self.advance();
15272 break;
15273 }
15274 other => {
15275 return Err(self.err(format!(
15276 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15277 )));
15278 }
15279 }
15280 }
15281 let modulus = modulus
15282 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15283 let remainder = remainder.ok_or_else(|| {
15284 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15285 })?;
15286 if modulus == 0 {
15287 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15288 }
15289 if remainder >= modulus {
15290 return Err(self.err(format!(
15291 "FOR VALUES WITH: REMAINDER ({remainder}) \
15292 must be < MODULUS ({modulus})"
15293 )));
15294 }
15295 PartitionOfBoundsAst::Hash { modulus, remainder }
15296 } else {
15297 match self.peek() {
15298 Token::From => {
15299 self.advance();
15300 let lower = Box::new(self.parse_partition_bound_expr()?);
15301 if !matches!(self.peek(), Token::To) {
15302 return Err(self.err(format!(
15303 "expected TO after FROM (...), got {:?}",
15304 self.peek()
15305 )));
15306 }
15307 self.advance();
15308 let upper = Box::new(self.parse_partition_bound_expr()?);
15309 PartitionOfBoundsAst::Range { lower, upper }
15310 }
15311 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15312 Token::In => {
15313 self.advance();
15314 if !matches!(self.peek(), Token::LParen) {
15315 return Err(self.err(format!(
15316 "expected '(' after FOR VALUES IN, got {:?}",
15317 self.peek()
15318 )));
15319 }
15320 self.advance();
15321 let mut values = Vec::new();
15322 loop {
15323 values.push(self.parse_expr(0)?);
15324 match self.peek() {
15325 Token::Comma => {
15326 self.advance();
15327 }
15328 Token::RParen => {
15329 self.advance();
15330 break;
15331 }
15332 other => {
15333 return Err(self.err(format!(
15334 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15335 )));
15336 }
15337 }
15338 }
15339 if values.is_empty() {
15340 return Err(self.err(
15341 "FOR VALUES IN requires at least one literal".to_string(),
15342 ));
15343 }
15344 PartitionOfBoundsAst::List { values }
15345 }
15346 other => {
15347 return Err(self.err(format!(
15348 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15349 )));
15350 }
15351 }
15352 }
15353 }
15354 other => {
15355 return Err(self.err(format!(
15356 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15357 )));
15358 }
15359 };
15360 Ok(PartitionOfSpec {
15361 parent_name,
15362 bounds,
15363 })
15364 }
15365
15366 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15367 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15368 /// markers (no-arg builtins) so the engine resolves them
15369 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15370 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15371 if !matches!(self.peek(), Token::LParen) {
15372 return Err(self.err(format!(
15373 "expected '(' before partition bound, got {:?}",
15374 self.peek()
15375 )));
15376 }
15377 self.advance();
15378 let expr = match self.peek() {
15379 Token::Ident(s) | Token::QuotedIdent(s)
15380 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15381 {
15382 let name = s.to_ascii_uppercase();
15383 self.advance();
15384 crate::ast::Expr::FunctionCall {
15385 name,
15386 args: Vec::new(),
15387 }
15388 }
15389 _ => self.parse_expr(0)?,
15390 };
15391 if !matches!(self.peek(), Token::RParen) {
15392 return Err(self.err(format!(
15393 "expected ')' after partition bound, got {:?}",
15394 self.peek()
15395 )));
15396 }
15397 self.advance();
15398 Ok(expr)
15399 }
15400
15401 /// v7.14.0 — true when the next tokens look like an inline
15402 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15403 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15404 /// — each followed by an optional name + `(...)`. Critical:
15405 /// a column NAMED `key` / `index` (PG accepts as ident) must
15406 /// NOT be mistaken for the KEY constraint shape. We disambig
15407 /// by requiring the keyword to be followed by either `(` or
15408 /// `<ident> (`.
15409 fn peek_mysql_inline_key_start(&self) -> bool {
15410 let cur = self.peek();
15411 // Shapes:
15412 // KEY (cols)
15413 // KEY name (cols)
15414 // INDEX (cols)
15415 // INDEX name (cols)
15416 // UNIQUE KEY [name] (cols)
15417 // UNIQUE INDEX [name] (cols)
15418 // FULLTEXT [KEY|INDEX] [name] (cols)
15419 // SPATIAL [KEY|INDEX] [name] (cols)
15420 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15421 // tokens at skip = the position AFTER the index-form
15422 // keywords (KEY/INDEX) have been consumed.
15423 match self.tokens.get(skip) {
15424 Some(Token::LParen) => true,
15425 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15426 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15427 }
15428 _ => false,
15429 }
15430 };
15431 // `INDEX` lexes as Token::Index (reserved), not as
15432 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15433 // start; the peek helper below handles either.
15434 let is_key_or_index_tok = |t: &Token| -> bool {
15435 matches!(t, Token::Index)
15436 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15437 };
15438 match cur {
15439 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15440 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15441 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15442 }
15443 Token::Ident(s)
15444 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15445 {
15446 let nxt = self.tokens.get(self.pos + 1);
15447 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15448 self.pos + 2
15449 } else {
15450 self.pos + 1
15451 };
15452 after_keyword_followed_by_paren_or_ident_paren(after_after)
15453 }
15454 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15455 let nxt = self.tokens.get(self.pos + 1);
15456 if !nxt.is_some_and(is_key_or_index_tok) {
15457 return false;
15458 }
15459 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15460 }
15461 _ => false,
15462 }
15463 }
15464
15465 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15466 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15467 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15468 /// returns Some(TableConstraint::Index) so the engine builds
15469 /// a real BTree index on the leading column (mysqldump
15470 /// `KEY idx_posts_author (author_id)` shape).
15471 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15472 /// (the storage layer has no matching AM).
15473 fn parse_mysql_inline_key(
15474 &mut self,
15475 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15476 // Detect UNIQUE prefix.
15477 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15478 {
15479 self.advance();
15480 true
15481 } else {
15482 false
15483 };
15484 // Consume FULLTEXT / SPATIAL prefix and record which one
15485 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15486 // dedicated TableConstraint variant so the engine can
15487 // build a tsvector-GIN; SPATIAL still has no matching
15488 // AM, so it falls back to accept-as-no-op.
15489 let mut is_fulltext = false;
15490 let mut is_spatial = false;
15491 if let Token::Ident(s) = self.peek().clone() {
15492 if s.eq_ignore_ascii_case("fulltext") {
15493 self.advance();
15494 is_fulltext = true;
15495 } else if s.eq_ignore_ascii_case("spatial") {
15496 self.advance();
15497 is_spatial = true;
15498 }
15499 }
15500 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15501 // (reserved); accept either token shape.
15502 match self.peek() {
15503 Token::Index => {
15504 self.advance();
15505 }
15506 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15507 self.advance();
15508 }
15509 other => {
15510 return Err(self.err(alloc::format!(
15511 "expected KEY/INDEX in inline index declaration, got {other:?}"
15512 )));
15513 }
15514 }
15515 // Optional index name (an ident before the `(`).
15516 // v7.15.0 — capture the name when present so the engine
15517 // builds the secondary index under the user's chosen
15518 // name (matches mysqldump's `KEY idx_x (col)` shape).
15519 let mut idx_name: Option<String> = None;
15520 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15521 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15522 {
15523 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15524 idx_name = Some(s);
15525 }
15526 }
15527 // Optional `USING BTREE` / `USING HASH` (MySQL).
15528 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15529 self.advance();
15530 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15531 self.advance();
15532 }
15533 }
15534 // Required column list `(col [, col]*)`.
15535 if !matches!(self.peek(), Token::LParen) {
15536 return Err(self.err(alloc::format!(
15537 "expected '(' in inline KEY/INDEX, got {:?}",
15538 self.peek()
15539 )));
15540 }
15541 self.advance();
15542 let mut cols: Vec<String> = Vec::new();
15543 let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15544 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15545 self.advance();
15546 cols.push(s);
15547 // v7.40.0 — the per-column `(length)` prefix is KEPT.
15548 //
15549 // It used to be skipped, so `KEY kb (b(4))` was accepted and
15550 // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15551 // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15552 // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15553 // then unrecorded is the worst of the three answers.
15554 let mut prefix: Option<u32> = None;
15555 if matches!(self.peek(), Token::LParen) {
15556 let mut depth = 1usize;
15557 self.advance();
15558 if let Token::Integer(n) = self.peek()
15559 && let Ok(v) = u32::try_from(*n)
15560 {
15561 prefix = Some(v);
15562 }
15563 while depth > 0 {
15564 match self.peek() {
15565 Token::LParen => depth += 1,
15566 Token::RParen => depth -= 1,
15567 Token::Eof => break,
15568 _ => {}
15569 }
15570 self.advance();
15571 }
15572 }
15573 prefix_lengths.push(prefix);
15574 // Skip optional ASC / DESC.
15575 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15576 || matches!(self.peek(), Token::Asc | Token::Desc)
15577 {
15578 self.advance();
15579 }
15580 if matches!(self.peek(), Token::Comma) {
15581 self.advance();
15582 continue;
15583 }
15584 break;
15585 }
15586 if matches!(self.peek(), Token::RParen) {
15587 self.advance();
15588 }
15589 // Trailing options on the inline index — comment / etc.
15590 // Skip until comma or `)`.
15591 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15592 self.advance();
15593 }
15594 if cols.is_empty() {
15595 return Ok(None);
15596 }
15597 if is_unique {
15598 // Carry the captured idx_name on UNIQUE too so future
15599 // engine work can name the underlying BTree
15600 // accordingly; today the unique-constraint installer
15601 // synthesises the name itself, but Display round-trip
15602 // benefits from preserving it.
15603 Ok(Some(crate::ast::TableConstraint::Unique {
15604 name: idx_name,
15605 columns: cols,
15606 nulls_not_distinct: false,
15607 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15608 deferrable: false,
15609 initially_deferred: false,
15610 prefix_lengths,
15611 }))
15612 } else if is_fulltext {
15613 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15614 // routes through `TableConstraint::FulltextIndex`;
15615 // the engine builds a tsvector-GIN over each named
15616 // column so MATCH AGAINST gets a real inverted
15617 // index instead of a silently-dropped declaration.
15618 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15619 name: idx_name,
15620 columns: cols,
15621 }))
15622 } else if is_spatial {
15623 // SPG has no native SPATIAL AM. Accept-as-no-op
15624 // (declaration is parsed, but no index is built).
15625 Ok(None)
15626 } else {
15627 // v7.15.0 — plain KEY / INDEX builds a real BTree
15628 // secondary index.
15629 Ok(Some(crate::ast::TableConstraint::Index {
15630 name: idx_name,
15631 columns: cols,
15632 prefix_lengths,
15633 }))
15634 }
15635 }
15636
15637 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15638 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15639 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15640 /// (in any order, separated by whitespace).
15641 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15642 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15643 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15644 /// bare ident here, and only the parenthesised form is reloptions (so this
15645 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15646 fn consume_with_reloptions(&mut self) {
15647 let is_with = matches!(
15648 self.peek(),
15649 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15650 );
15651 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15652 return;
15653 }
15654 self.advance(); // WITH
15655 self.advance(); // (
15656 let mut depth = 1u32;
15657 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15658 match self.peek() {
15659 Token::LParen => depth += 1,
15660 Token::RParen => depth -= 1,
15661 _ => {}
15662 }
15663 self.advance();
15664 }
15665 }
15666
15667 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15668 /// dropped with everything else here. The rest of the MySQL table
15669 /// options genuinely have no meaning for SPG's storage; the engine
15670 /// name does, because MySQL REFUSES one it does not know and a dump
15671 /// with a typo in it should not quietly become a table.
15672 fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15673 let mut engine: Option<alloc::string::String> = None;
15674 let mut auto_increment: Option<i64> = None;
15675 loop {
15676 // Heuristic: a table option is an ident (or `DEFAULT`
15677 // reserved keyword) followed by `=` and an
15678 // ident / string / integer.
15679 let name_lc = match self.peek().clone() {
15680 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15681 Token::Default => alloc::string::String::from("default"),
15682 _ => break,
15683 };
15684 let known = matches!(
15685 name_lc.as_str(),
15686 "engine"
15687 | "default"
15688 | "charset"
15689 | "collate"
15690 | "auto_increment"
15691 | "row_format"
15692 | "comment"
15693 | "pack_keys"
15694 | "stats_persistent"
15695 | "stats_auto_recalc"
15696 | "stats_sample_pages"
15697 | "key_block_size"
15698 | "tablespace"
15699 | "min_rows"
15700 | "max_rows"
15701 | "checksum"
15702 | "delay_key_write"
15703 | "insert_method"
15704 | "data"
15705 | "index"
15706 | "encryption"
15707 | "compression"
15708 );
15709 if !known {
15710 break;
15711 }
15712 self.advance(); // option name
15713 // `DEFAULT` optional prefix is followed by `CHARSET` /
15714 // `COLLATE`; consume the next ident too.
15715 if name_lc == "default" {
15716 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15717 self.advance();
15718 }
15719 }
15720 if matches!(self.peek(), Token::Eq) {
15721 self.advance();
15722 }
15723 match self.peek().clone() {
15724 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15725 if name_lc == "engine" {
15726 // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15727 // engine it does not know and names it back
15728 // exactly: `Unknown storage engine 'NoSuchEng'`,
15729 // measured. The lexer folds a bare identifier, so
15730 // the message quoted a name the dump did not
15731 // contain, which is the one thing that message is
15732 // for. Guarded the same way the column spelling
15733 // is: the span runs to the next token, so what
15734 // comes back has to be the same word.
15735 let written = self
15736 .source_span(self.pos, self.pos)
15737 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15738 .filter(|raw| raw.eq_ignore_ascii_case(&v))
15739 .map(alloc::string::String::from);
15740 engine = Some(written.unwrap_or(v));
15741 }
15742 self.advance();
15743 }
15744 Token::Integer(v) => {
15745 // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15746 // the table hands out, and it was consumed and
15747 // dropped: measured on MySQL 9.7.2, the first row
15748 // inserted into a table declared that way gets 100,
15749 // where SPG gave it 1. `SHOW CREATE TABLE` already
15750 // reproduces the option from the counter, so the
15751 // dump round-tripped through a different number.
15752 if name_lc == "auto_increment" {
15753 auto_increment = Some(v);
15754 }
15755 self.advance();
15756 }
15757 _ => {}
15758 }
15759 }
15760 (engine, auto_increment)
15761 }
15762
15763 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15764 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15765 /// sure (otherwise a column literally named `primary` would
15766 /// be mistaken).
15767 fn peek_table_level_pk_start(&self) -> bool {
15768 let cur = self.peek();
15769 let nxt = self.tokens.get(self.pos + 1);
15770 let nxt2 = self.tokens.get(self.pos + 2);
15771 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15772 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15773 let is_lparen = matches!(nxt2, Some(Token::LParen));
15774 is_primary && is_key && is_lparen
15775 }
15776
15777 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15778 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15779 /// (mailrs round-5 G10).
15780 fn peek_table_level_unique_start(&self) -> bool {
15781 let cur = self.peek();
15782 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15783 if !is_unique {
15784 return false;
15785 }
15786 let n1 = self.tokens.get(self.pos + 1);
15787 // Plain `UNIQUE (…)`.
15788 if matches!(n1, Some(Token::LParen)) {
15789 return true;
15790 }
15791 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15792 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15793 if !is_nulls {
15794 return false;
15795 }
15796 let n2 = self.tokens.get(self.pos + 2);
15797 let n3 = self.tokens.get(self.pos + 3);
15798 let n4 = self.tokens.get(self.pos + 4);
15799 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15800 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15801 return true;
15802 }
15803 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15804 if matches!(n2, Some(Token::Not))
15805 && matches!(n3, Some(Token::Distinct))
15806 && matches!(n4, Some(Token::LParen))
15807 {
15808 return true;
15809 }
15810 false
15811 }
15812
15813 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15814 self.advance(); // PRIMARY
15815 self.advance(); // KEY
15816 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15817 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15818 // 621 consumed and dropped them (the storing half of F08).
15819 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15820 Ok(crate::ast::TableConstraint::PrimaryKey {
15821 name: None,
15822 columns,
15823 deferrable,
15824 initially_deferred,
15825 })
15826 }
15827
15828 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15829 self.advance(); // UNIQUE
15830 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15831 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15832 // is `NULLS DISTINCT` per the SQL standard.
15833 let mut nulls_not_distinct = false;
15834 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15835 let n1 = self.tokens.get(self.pos + 1);
15836 let n2 = self.tokens.get(self.pos + 2);
15837 let is_not = matches!(n1, Some(Token::Not));
15838 let is_distinct = matches!(n2, Some(Token::Distinct));
15839 if is_not && is_distinct {
15840 self.advance(); // NULLS
15841 self.advance(); // NOT
15842 self.advance(); // DISTINCT
15843 nulls_not_distinct = true;
15844 } else if matches!(n1, Some(Token::Distinct)) {
15845 self.advance(); // NULLS
15846 self.advance(); // DISTINCT
15847 }
15848 }
15849 let columns = self.parse_paren_ident_list("UNIQUE")?;
15850 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15851 Ok(crate::ast::TableConstraint::Unique {
15852 name: None,
15853 columns,
15854 nulls_not_distinct,
15855 deferrable,
15856 initially_deferred,
15857 prefix_lengths: Vec::new(),
15858 })
15859 }
15860
15861 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15862 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15863 /// expression.
15864 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15865 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15866 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15867 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15868 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15869 /// commit: `NOT` starts no other suffix here, but reading both
15870 /// tokens before advancing keeps the caller's error message intact
15871 /// if someone writes `NOT NULL` by mistake.
15872 fn parse_not_valid_suffix(&mut self) -> bool {
15873 if !matches!(self.peek(), Token::Not) {
15874 return false;
15875 }
15876 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15877 {
15878 return false;
15879 }
15880 self.advance();
15881 self.advance();
15882 true
15883 }
15884
15885 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15886 self.advance(); // EXCLUDE
15887 // Optional `USING <method>`.
15888 let mut method = None;
15889 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15890 self.advance();
15891 method = Some(match self.advance() {
15892 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15893 other => {
15894 return Err(self.err(alloc::format!(
15895 "expected index method after USING, got {other:?}"
15896 )));
15897 }
15898 });
15899 }
15900 if !matches!(self.peek(), Token::LParen) {
15901 return Err(self.err(alloc::format!(
15902 "expected '(' after EXCLUDE, got {:?}",
15903 self.peek()
15904 )));
15905 }
15906 self.advance();
15907 let mut elements: Vec<(String, String)> = Vec::new();
15908 loop {
15909 let col = match self.advance() {
15910 Token::Ident(s) | Token::QuotedIdent(s) => s,
15911 other => {
15912 return Err(self.err(alloc::format!(
15913 "expected column name in EXCLUDE, got {other:?}"
15914 )));
15915 }
15916 };
15917 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15918 return Err(self.err(alloc::format!(
15919 "expected WITH after EXCLUDE column, got {:?}",
15920 self.peek()
15921 )));
15922 }
15923 self.advance();
15924 let op = match self.advance() {
15925 Token::InetOverlap => String::from("&&"),
15926 Token::Intersects => String::from("?#"),
15927 Token::IsBelow => String::from("<^"),
15928 Token::IsAbove => String::from(">^"),
15929 Token::PatternLt => String::from("~<~"),
15930 Token::PatternLtEq => String::from("~<=~"),
15931 Token::PatternGt => String::from("~>~"),
15932 Token::PatternGtEq => String::from("~>=~"),
15933 Token::TsMatchOld => String::from("@@@"),
15934 Token::Eq => String::from("="),
15935 Token::JsonContains => String::from("@>"),
15936 Token::JsonContainedBy => String::from("<@"),
15937 Token::OverLeft => String::from("&<"),
15938 Token::OverRight => String::from("&>"),
15939 other => {
15940 return Err(self.err(alloc::format!(
15941 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15942 )));
15943 }
15944 };
15945 elements.push((col, op));
15946 if matches!(self.peek(), Token::Comma) {
15947 self.advance();
15948 continue;
15949 }
15950 break;
15951 }
15952 if !matches!(self.peek(), Token::RParen) {
15953 return Err(self.err(alloc::format!(
15954 "expected ')' to close EXCLUDE, got {:?}",
15955 self.peek()
15956 )));
15957 }
15958 self.advance();
15959 Ok(crate::ast::TableConstraint::Exclude {
15960 name: None,
15961 method,
15962 elements,
15963 })
15964 }
15965
15966 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15967 self.advance(); // CHECK
15968 if !matches!(self.peek(), Token::LParen) {
15969 return Err(self.err(alloc::format!(
15970 "expected '(' after CHECK, got {:?}",
15971 self.peek()
15972 )));
15973 }
15974 self.advance();
15975 let expr = self.parse_expr(0)?;
15976 if !matches!(self.peek(), Token::RParen) {
15977 return Err(self.err(alloc::format!(
15978 "expected ')' to close CHECK predicate, got {:?}",
15979 self.peek()
15980 )));
15981 }
15982 self.advance();
15983 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15984 // are no existing rows for PG to skip, so it rejects the suffix.
15985 Ok(crate::ast::TableConstraint::Check {
15986 name: None,
15987 expr,
15988 not_valid: false,
15989 })
15990 }
15991
15992 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15993 fn peek_table_level_check_start(&self) -> bool {
15994 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15995 }
15996
15997 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15998 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15999 /// on the dedicated FK path (`parse_table_level_fk` consumes its
16000 /// own CONSTRAINT prefix).
16001 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16002 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16003 return None;
16004 }
16005 // tokens[pos+1] is the constraint name (any ident-like);
16006 // tokens[pos+2] is the kind keyword.
16007 match self.tokens.get(self.pos + 2) {
16008 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16009 Some(NamedTableConstraintKind::Check)
16010 }
16011 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16012 Some(NamedTableConstraintKind::Unique)
16013 }
16014 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16015 Some(NamedTableConstraintKind::PrimaryKey)
16016 }
16017 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16018 Some(NamedTableConstraintKind::Exclude)
16019 }
16020 _ => None,
16021 }
16022 }
16023
16024 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16025 if !matches!(self.peek(), Token::LParen) {
16026 return Err(self.err(alloc::format!(
16027 "expected '(' after {ctx}, got {:?}",
16028 self.peek()
16029 )));
16030 }
16031 self.advance();
16032 let mut out = Vec::new();
16033 loop {
16034 out.push(self.expect_ident_like()?);
16035 match self.peek() {
16036 Token::Comma => {
16037 self.advance();
16038 }
16039 Token::RParen => {
16040 self.advance();
16041 break;
16042 }
16043 other => {
16044 return Err(self.err(alloc::format!(
16045 "expected ',' or ')' in {ctx} list, got {other:?}"
16046 )));
16047 }
16048 }
16049 }
16050 if out.is_empty() {
16051 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16052 }
16053 Ok(out)
16054 }
16055
16056 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16057 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16058 /// table-level FK; a column def never starts with either keyword
16059 /// (column names are not in this reserved set).
16060 fn peek_constraint_or_fk_start(&self) -> bool {
16061 let is_constraint_kw = matches!(
16062 self.peek(),
16063 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16064 );
16065 let is_foreign_kw = matches!(
16066 self.peek(),
16067 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16068 );
16069 is_constraint_kw || is_foreign_kw
16070 }
16071
16072 /// v7.6.0 — parse a table-level FK clause:
16073 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16074 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16075 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16076 let mut name: Option<String> = None;
16077 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16078 self.advance();
16079 name = Some(self.expect_ident_like()?);
16080 }
16081 // `FOREIGN`
16082 match self.advance() {
16083 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16084 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16085 }
16086 // `KEY`
16087 match self.advance() {
16088 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16089 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16090 }
16091 // `(col, col, ...)`
16092 if !matches!(self.peek(), Token::LParen) {
16093 return Err(self.err(format!(
16094 "expected '(' after FOREIGN KEY, got {:?}",
16095 self.peek()
16096 )));
16097 }
16098 self.advance();
16099 let mut columns = Vec::new();
16100 loop {
16101 columns.push(self.expect_ident_like()?);
16102 match self.peek() {
16103 Token::Comma => {
16104 self.advance();
16105 }
16106 Token::RParen => {
16107 self.advance();
16108 break;
16109 }
16110 other => {
16111 return Err(self.err(format!(
16112 "expected ',' or ')' in FK column list, got {other:?}"
16113 )));
16114 }
16115 }
16116 }
16117 if columns.is_empty() {
16118 return Err(self.err("FOREIGN KEY requires at least one column".into()));
16119 }
16120 let (
16121 parent_table,
16122 parent_columns,
16123 on_delete,
16124 on_update,
16125 match_type,
16126 deferrable,
16127 initially_deferred,
16128 ) = self.parse_references_tail(columns.len())?;
16129 Ok(ForeignKeyConstraint {
16130 name,
16131 columns,
16132 parent_table,
16133 parent_columns,
16134 on_delete,
16135 on_update,
16136 match_type,
16137 deferrable,
16138 initially_deferred,
16139 })
16140 }
16141
16142 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16143 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16144 /// the local column count, used to default the parent column
16145 /// list when omitted (SQL spec: parent's PK is implied).
16146 fn parse_references_tail(
16147 &mut self,
16148 expected_arity: usize,
16149 ) -> Result<
16150 (
16151 String,
16152 Vec<String>,
16153 FkAction,
16154 FkAction,
16155 crate::ast::MatchType,
16156 // v7.39 (round 288) — deferrable, initially_deferred.
16157 bool,
16158 bool,
16159 ),
16160 ParseError,
16161 > {
16162 match self.advance() {
16163 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16164 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16165 }
16166 let parent_table = self.expect_ident_like()?;
16167 let mut parent_columns: Vec<String> = Vec::new();
16168 if matches!(self.peek(), Token::LParen) {
16169 self.advance();
16170 loop {
16171 parent_columns.push(self.expect_ident_like()?);
16172 match self.peek() {
16173 Token::Comma => {
16174 self.advance();
16175 }
16176 Token::RParen => {
16177 self.advance();
16178 break;
16179 }
16180 other => {
16181 return Err(self.err(format!(
16182 "expected ',' or ')' in REFERENCES column list, got {other:?}"
16183 )));
16184 }
16185 }
16186 }
16187 }
16188 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16189 return Err(self.err(format!(
16190 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16191 expected_arity,
16192 parent_columns.len()
16193 )));
16194 }
16195 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16196 // it between the referenced column list and the ON / DEFERRABLE
16197 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16198 // is skipped when any referencing column is NULL), so SIMPLE —
16199 // the default, and the only spelling pg_dump emits — is accepted
16200 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16201 // mixed-NULL rule, which is not wired yet; reject them honestly
16202 // rather than silently applying SIMPLE (PG itself errors on
16203 // MATCH PARTIAL as "not yet implemented").
16204 let mut match_type = crate::ast::MatchType::Simple;
16205 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16206 self.advance();
16207 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16208 // SIMPLE / PARTIAL arrive as bare identifiers.
16209 let kind = match self.advance() {
16210 Token::Full => "FULL".to_string(),
16211 Token::Ident(s) => s.to_uppercase(),
16212 other => {
16213 return Err(self.err(format!(
16214 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16215 )));
16216 }
16217 };
16218 match kind.as_str() {
16219 "SIMPLE" => {} // Default — match_type stays Simple.
16220 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16221 // when ALL referencing columns are NULL; a mixed-NULL key errors.
16222 "FULL" => match_type = crate::ast::MatchType::Full,
16223 "PARTIAL" => {
16224 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16225 }
16226 _ => {
16227 return Err(self.err(format!(
16228 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16229 )));
16230 }
16231 }
16232 }
16233 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16234 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16235 // <action>` / `ON UPDATE <action>` in either order. PG /
16236 // pg_dump emits the timing clause AFTER the ON clauses
16237 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16238 // but the SQL spec allows either order. We loop over
16239 // every possible trailer and dispatch on the next token,
16240 // stopping when nothing matches. Phase 3.1 changes the
16241 // bare DEFERRABLE form from hard-error to accept-as-
16242 // immediate; SPG is single-writer with no deferred-
16243 // constraint window so the runtime semantics are always
16244 // immediate even when INITIALLY DEFERRED is requested.
16245 // PG's default referential action (no ON DELETE / ON UPDATE
16246 // clause) is NO ACTION, not RESTRICT — the two enforce
16247 // identically in SPG (single-writer, no deferred window; see the
16248 // shared match arm in constraints.rs) but information_schema.
16249 // referential_constraints must report NO ACTION to match PG.
16250 let mut on_delete = FkAction::NoAction;
16251 let mut on_update = FkAction::NoAction;
16252 let mut seen_on_delete = false;
16253 let mut seen_on_update = false;
16254 let mut deferrable = false;
16255 let mut initially_deferred = false;
16256 loop {
16257 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16258 let before = self.pos;
16259 let (d, idef) = self.consume_deferrable_clauses_timed()?;
16260 if self.pos != before {
16261 deferrable = d;
16262 initially_deferred = idef;
16263 continue;
16264 }
16265 // ON DELETE / ON UPDATE.
16266 if !matches!(self.peek(), Token::On) {
16267 break;
16268 }
16269 self.advance();
16270 let which = self.advance();
16271 let action = self.parse_fk_action()?;
16272 match which {
16273 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16274 if seen_on_delete {
16275 return Err(self.err("ON DELETE specified twice".into()));
16276 }
16277 seen_on_delete = true;
16278 on_delete = action;
16279 }
16280 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16281 if seen_on_update {
16282 return Err(self.err("ON UPDATE specified twice".into()));
16283 }
16284 seen_on_update = true;
16285 on_update = action;
16286 }
16287 other => {
16288 return Err(
16289 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16290 );
16291 }
16292 }
16293 }
16294 Ok((
16295 parent_table,
16296 parent_columns,
16297 on_delete,
16298 on_update,
16299 match_type,
16300 deferrable,
16301 initially_deferred,
16302 ))
16303 }
16304
16305 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16306 /// NO ACTION`.
16307 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16308 match self.advance() {
16309 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16310 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16311 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16312 Token::Null => Ok(FkAction::SetNull),
16313 Token::Default => Ok(FkAction::SetDefault),
16314 other => Err(self.err(format!(
16315 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16316 ))),
16317 },
16318 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16319 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16320 other => Err(self.err(format!(
16321 "expected ACTION after NO in FK action, got {other:?}"
16322 ))),
16323 },
16324 other => Err(self.err(format!(
16325 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16326 ))),
16327 }
16328 }
16329
16330 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16331 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16332 fn consume_if_not_exists(&mut self) -> bool {
16333 // `IF` arrives as a bare Ident (we don't reserve it because it
16334 // also appears mid-expression in PG, though we don't support
16335 // those forms yet).
16336 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16337 if !looks_like_if {
16338 return false;
16339 }
16340 // Peek one ahead before committing: only consume IF when it's
16341 // actually `IF NOT EXISTS`.
16342 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16343 return false;
16344 }
16345 if !matches!(
16346 self.tokens.get(self.pos + 2),
16347 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16348 ) {
16349 return false;
16350 }
16351 self.advance(); // IF
16352 self.advance(); // NOT
16353 self.advance(); // EXISTS
16354 true
16355 }
16356
16357 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16358 /// Consumes IF EXISTS as a pair; returns false otherwise
16359 /// without consuming any tokens.
16360 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16361 /// ENABLE/DISABLE/FORCE/NO FORCE.
16362 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16363 for kw in ["row", "level", "security"] {
16364 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16365 {
16366 return Err(self.err(alloc::format!(
16367 "expected {} in ROW LEVEL SECURITY, got {:?}",
16368 kw.to_ascii_uppercase(),
16369 self.peek()
16370 )));
16371 }
16372 self.advance();
16373 }
16374 Ok(())
16375 }
16376
16377 fn consume_if_exists(&mut self) -> bool {
16378 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16379 if !looks_like_if {
16380 return false;
16381 }
16382 if !matches!(
16383 self.tokens.get(self.pos + 1),
16384 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16385 ) {
16386 return false;
16387 }
16388 self.advance(); // IF
16389 self.advance(); // EXISTS
16390 true
16391 }
16392
16393 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16394 /// qualifiers after an index column ref. ASC / DESC are
16395 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16396 /// We accept and discard them since single-column BTree
16397 /// stores rows in natural key order today.
16398 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16399 /// ORDER BY key. Returns None when absent.
16400 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16401 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16402 return Ok(None);
16403 }
16404 self.advance();
16405 match self.advance() {
16406 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16407 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16408 other => Err(self.err(alloc::format!(
16409 "expected FIRST or LAST after NULLS, got {other:?}"
16410 ))),
16411 }
16412 }
16413
16414 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16415 /// rather than discarded.
16416 ///
16417 /// SPG's index does not scan in a direction — column ordering is
16418 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16419 /// reproduction of the DDL, and dropping the clause meant
16420 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16421 /// dump lost it, and a schema diff saw drift on every run.
16422 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16423 let mut order = crate::ast::IndexColumnOrder::default();
16424 loop {
16425 match self.peek() {
16426 Token::Asc => {
16427 self.advance();
16428 }
16429 Token::Desc => {
16430 order.descending = true;
16431 self.advance();
16432 }
16433 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16434 let look = self.tokens.get(self.pos + 1);
16435 if matches!(
16436 look,
16437 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16438 || k.eq_ignore_ascii_case("last")
16439 ) {
16440 self.advance();
16441 order.nulls_first = Some(matches!(
16442 self.advance(),
16443 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16444 ));
16445 } else {
16446 break;
16447 }
16448 }
16449 _ => break,
16450 }
16451 }
16452 order
16453 }
16454
16455 fn parse_create_index_stmt_after_create(
16456 &mut self,
16457 is_unique: bool,
16458 ) -> Result<Statement, ParseError> {
16459 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16460 debug_assert!(matches!(self.peek(), Token::Index));
16461 self.advance();
16462 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16463 // SPG's CREATE INDEX is synchronous end-to-end today (real
16464 // CONCURRENTLY variant with restartable scans queues with
16465 // v7.39 indexes epic), so the modifier has no runtime effect
16466 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16467 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16468 // VIEW CONCURRENTLY.
16469 let mut concurrently = false;
16470 if matches!(
16471 self.peek(),
16472 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16473 ) {
16474 self.advance();
16475 concurrently = true;
16476 }
16477 let if_not_exists = self.consume_if_not_exists();
16478 // v7.39 (read01 round 93) — the index name is optional (PG since
16479 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16480 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16481 // was given; leave it empty and the engine derives a PG-style
16482 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16483 let name = if matches!(self.peek(), Token::On) {
16484 String::new()
16485 } else {
16486 self.expect_ident_like()?
16487 };
16488 if !matches!(self.peek(), Token::On) {
16489 return Err(self.err(format!(
16490 "expected ON after CREATE INDEX <name>, got {:?}",
16491 self.peek()
16492 )));
16493 }
16494 self.advance();
16495 let table = self.expect_ident_like()?;
16496 // Optional `USING <method>` — only recognised method in v2.0 is
16497 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16498 // ident `using` (we don't promote it to a reserved keyword
16499 // because it isn't reserved anywhere else in our SQL surface).
16500 let mut method_name: Option<String> = None;
16501 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16502 self.advance();
16503 let m = self.expect_ident_like()?;
16504 method_name = Some(m.to_ascii_lowercase());
16505 match m.to_ascii_lowercase().as_str() {
16506 "hnsw" => IndexMethod::Hnsw,
16507 "btree" => IndexMethod::BTree,
16508 "brin" => IndexMethod::Brin,
16509 // v7.12.3 — real GIN inverted index over `tsvector`.
16510 // v7.9.26b's `USING gin` → BTree silent fallback is
16511 // gone; the engine validates that the indexed column
16512 // is `tsvector` at CREATE INDEX time.
16513 "gin" => IndexMethod::Gin,
16514 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16515 // `USING spgist` / `USING hash` for their built-in
16516 // AMs that SPG doesn't have a matching
16517 // implementation for; degrade to BTree on the
16518 // leading column so the schema loads + the index
16519 // catalogue stays consistent. Operator pays the
16520 // planner cost only for the queries that would have
16521 // used the specialised AM.
16522 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16523 // v7.11.3 — pgvector ships both `ivfflat` and
16524 // `hnsw`. Customers shouldn't have to choose
16525 // their on-disk index method based on what SPG
16526 // implements; accept `ivfflat` as a synonym for
16527 // `hnsw` so PG schemas using either method drop
16528 // in. The vector distance op (`<->` / `<#>` /
16529 // `<=>`) at query time still picks the metric.
16530 "ivfflat" => IndexMethod::Hnsw,
16531 other => {
16532 return Err(self.err(alloc::format!(
16533 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16534 )));
16535 }
16536 }
16537 } else {
16538 IndexMethod::BTree
16539 };
16540 if !matches!(self.peek(), Token::LParen) {
16541 return Err(self.err(format!(
16542 "expected '(' before indexed column, got {:?}",
16543 self.peek()
16544 )));
16545 }
16546 self.advance();
16547 // v6.8.2 — accept either a bare column ident (legacy) or
16548 // an expression `fn(col, …)` for expression indexes.
16549 // Distinguish by peeking the token *after* the current
16550 // ident: `ident )` is the legacy column-only path;
16551 // anything else triggers the Pratt expression parser.
16552 // (`advance()` uses `mem::replace` to nil out the current
16553 // slot, so we can't save+rewind cleanly — peek-ahead via
16554 // direct index avoids the mutation.)
16555 let mut opclass: Option<String> = None;
16556 let mut key_collation: Option<String> = None;
16557 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16558 // Single column with `)` immediately after — fast path.
16559 // v7.9.29 — also: bare column followed by `,` (the
16560 // multi-column form `(a, b, c)`). Without this branch
16561 // the leading ident gets pulled into `parse_expr`
16562 // which then sets `expression = Some(Column(a))` and
16563 // breaks Display round-trip on the multi-column shape.
16564 Token::Ident(s) | Token::QuotedIdent(s)
16565 if matches!(
16566 self.tokens.get(self.pos + 1),
16567 Some(Token::RParen | Token::Comma)
16568 ) =>
16569 {
16570 self.advance();
16571 (s, None)
16572 }
16573 // v7.9.22 — single column followed by a pgvector
16574 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16575 // v7.15.0 — capture the opclass instead of discarding
16576 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16577 // → real trigram-shingle GIN over a TEXT column).
16578 // Vector/HNSW opclasses still take their distance
16579 // metric from the query operator (`<->` / `<#>` /
16580 // `<=>`), so for those callers the opclass stays
16581 // informational.
16582 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16583 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16584 // the schema and dispatch on the bare opclass, the same
16585 // treatment table/type names get.
16586 Token::Ident(s) | Token::QuotedIdent(s)
16587 if matches!(
16588 self.tokens.get(self.pos + 1),
16589 Some(Token::Ident(_) | Token::QuotedIdent(_))
16590 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16591 && matches!(
16592 self.tokens.get(self.pos + 3),
16593 Some(Token::Ident(op) | Token::QuotedIdent(op))
16594 if is_vector_opclass_name(op)
16595 ) =>
16596 {
16597 self.advance(); // column name
16598 self.advance(); // schema qualifier
16599 self.advance(); // dot
16600 let op_tok = self.advance();
16601 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16602 opclass = Some(op.to_ascii_lowercase());
16603 }
16604 (s, None)
16605 }
16606 // r1038 — an operator class is recognised by its POSITION, not
16607 // by a list of names. It used to be `is_vector_opclass_name`,
16608 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16609 // sentori's migration wrote — was a syntax error while
16610 // `USING gin (doc)` parsed. Anything sitting between a column
16611 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16612 // two bare identifiers in a row are not valid there otherwise.
16613 Token::Ident(s) | Token::QuotedIdent(s)
16614 if matches!(
16615 self.tokens.get(self.pos + 1),
16616 Some(Token::Ident(op) | Token::QuotedIdent(op))
16617 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16618 self.tokens.get(self.pos + 2)
16619 )
16620 ) =>
16621 {
16622 self.advance(); // column name
16623 // Capture the opclass token, lower-cased for
16624 // case-insensitive engine dispatch.
16625 let op_tok = self.advance();
16626 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16627 opclass = Some(op.to_ascii_lowercase());
16628 }
16629 (s, None)
16630 }
16631 Token::Ident(_) | Token::QuotedIdent(_) => {
16632 // v7.39 (round 538) — an explicit COLLATE on the key,
16633 // read by LOOKAHEAD because `parse_expr` absorbs the
16634 // clause as a no-op (SPG orders text by bytes, which is
16635 // the C collation, so it changes nothing to honour). PG
16636 // still PRINTS it: an explicitly written `"C"` and the
16637 // collation a column inherits are different collation
16638 // OBJECTS even where they sort identically, which is why
16639 // `(a COLLATE "C")` shows on a C-collation database too.
16640 if matches!(
16641 self.tokens.get(self.pos + 1),
16642 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16643 ) {
16644 key_collation = match self.tokens.get(self.pos + 2) {
16645 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16646 Some(n.clone())
16647 }
16648 _ => None,
16649 };
16650 }
16651 // v7.39.2 — the clause is read by the LOOKAHEAD above and
16652 // belongs to the KEY, not to the expression. Since
16653 // `COLLATE` became a node, letting `parse_expr` build one
16654 // here put the collation in twice and the key deparsed as
16655 // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16656 // is the same idea and already exists, so this borrows it:
16657 // absorb into the side channel, and the key's own
16658 // lookahead is what carries it.
16659 // v7.39.2 — and the key can only CARRY the byte-order
16660 // spellings. Absorbing into the side channel accepts any
16661 // name, so suppressing the node here without this check
16662 // silently accepted `(name COLLATE "en_US")`, which SPG's
16663 // index cannot honour — a refusal that was doing real
16664 // work, removed by the suppression and put back here.
16665 if let Some(name) = &key_collation {
16666 let lc = name.to_ascii_lowercase();
16667 let byte_order = matches!(
16668 lc.as_str(),
16669 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16670 );
16671 let mysql_ok = self.mysql_dialect
16672 && (lc.ends_with("_ci")
16673 || lc.ends_with("_bin")
16674 || lc == "binary"
16675 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16676 if !byte_order && !mysql_ok {
16677 return Err(self.err(alloc::format!(
16678 "COLLATE {name:?} is not supported in this position: an index \
16679 key carries the byte-order spellings only. Declare it on the \
16680 column (`x text COLLATE {name:?}`) instead"
16681 )));
16682 }
16683 }
16684 let saved_key_ctx = self.in_order_by_key;
16685 self.in_order_by_key = true;
16686 let key_expr = self.parse_expr(0);
16687 self.in_order_by_key = saved_key_ctx;
16688 let key_expr = key_expr?;
16689 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16690 self.err("expression index key must reference at least one column".into())
16691 })?;
16692 (primary, Some(key_expr))
16693 }
16694 // v7.37.43-T4 — parenthesised expression index key
16695 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16696 // PG's CREATE INDEX requires the expression to be in
16697 // its own parens to disambiguate function calls from
16698 // column lists, so this `LParen` is the inner open-paren
16699 // of an expression key. parse_expr handles the recursive
16700 // descent and consumes the matching `RParen`.
16701 Token::LParen => {
16702 let key_expr = self.parse_expr(0)?;
16703 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16704 self.err("expression index key must reference at least one column".into())
16705 })?;
16706 (primary, Some(key_expr))
16707 }
16708 other => {
16709 return Err(self.err(format!(
16710 "expected column ident or expression, got {other:?}"
16711 )));
16712 }
16713 };
16714 // v7.9.14 — accept extra comma-separated columns inside
16715 // the index key parens (`CREATE INDEX … (a, b, c)`).
16716 // mailrs F2.
16717 //
16718 // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16719 // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16720 // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16721 // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16722 // schema diff saw drift on every run. Reported by sentori
16723 // against 7.39.10, and the same defect round 537 fixed for the
16724 // LEADING column, in the loop right beside it.
16725 let mut extra_columns: Vec<String> = Vec::new();
16726 let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16727 // The leading column may also have ASC/DESC after it — and that
16728 // one is the column SPG indexes, so its clause is kept.
16729 let key_order = self.consume_optional_index_column_qualifiers();
16730 while matches!(self.peek(), Token::Comma) {
16731 self.advance();
16732 let extra = self.expect_ident_like()?;
16733 extra_orders.push(self.consume_optional_index_column_qualifiers());
16734 extra_columns.push(extra);
16735 }
16736 if !matches!(self.peek(), Token::RParen) {
16737 return Err(self.err(format!(
16738 "expected ')' after indexed column / expression, got {:?}",
16739 self.peek()
16740 )));
16741 }
16742 self.advance();
16743 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16744 // index-only-scan annotation. Bare ident (not a reserved
16745 // keyword) so we test by case-insensitive string match.
16746 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16747 {
16748 self.advance();
16749 if !matches!(self.peek(), Token::LParen) {
16750 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16751 }
16752 self.advance();
16753 let mut cols = Vec::new();
16754 loop {
16755 cols.push(self.expect_ident_like()?);
16756 match self.peek() {
16757 Token::Comma => {
16758 self.advance();
16759 }
16760 Token::RParen => {
16761 self.advance();
16762 break;
16763 }
16764 other => {
16765 return Err(self.err(format!(
16766 "expected ',' or ')' in INCLUDE list, got {other:?}"
16767 )));
16768 }
16769 }
16770 }
16771 cols
16772 } else {
16773 Vec::new()
16774 };
16775 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16776 // storage parameters. pgvector emits `WITH (lists = N)` for
16777 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16778 // SPG's HNSW picks its own parameters today (tunable via
16779 // env vars), so the WITH clause is informational and dropped.
16780 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16781 self.advance();
16782 if !matches!(self.peek(), Token::LParen) {
16783 return Err(self.err(format!(
16784 "expected '(' after WITH in CREATE INDEX, got {:?}",
16785 self.peek()
16786 )));
16787 }
16788 self.advance();
16789 loop {
16790 if matches!(self.peek(), Token::RParen) {
16791 self.advance();
16792 break;
16793 }
16794 // Drain `key = value` or bare `key` tokens.
16795 let _ = self.advance(); // key
16796 if matches!(self.peek(), Token::Eq) {
16797 self.advance();
16798 let _ = self.advance(); // value (int / string / ident)
16799 }
16800 match self.peek() {
16801 Token::Comma => {
16802 self.advance();
16803 }
16804 Token::RParen => {
16805 self.advance();
16806 break;
16807 }
16808 other => {
16809 return Err(self.err(format!(
16810 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16811 )));
16812 }
16813 }
16814 }
16815 }
16816 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16817 // which sits between the key list and the WHERE clause.
16818 let mut nulls_not_distinct = false;
16819 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16820 let n1 = self.tokens.get(self.pos + 1);
16821 let n2 = self.tokens.get(self.pos + 2);
16822 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16823 self.advance(); // NULLS
16824 self.advance(); // NOT
16825 self.advance(); // DISTINCT
16826 nulls_not_distinct = true;
16827 } else if matches!(n1, Some(Token::Distinct)) {
16828 self.advance(); // NULLS
16829 self.advance(); // DISTINCT
16830 }
16831 }
16832 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16833 let partial_predicate = if matches!(self.peek(), Token::Where) {
16834 self.advance();
16835 Some(self.parse_expr(0)?)
16836 } else {
16837 None
16838 };
16839 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16840 // sense: uniqueness over an ANN structure has no clean
16841 // semantics. Reject early. (BRIN UNIQUE is similarly
16842 // meaningless — block both.)
16843 if is_unique && !matches!(method, IndexMethod::BTree) {
16844 return Err(self.err(alloc::format!(
16845 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16846 method
16847 )));
16848 }
16849 Ok(Statement::CreateIndex(CreateIndexStatement {
16850 concurrently,
16851 name,
16852 key_order,
16853 key_collation,
16854 table,
16855 column,
16856 nulls_not_distinct,
16857 method,
16858 if_not_exists,
16859 included_columns,
16860 partial_predicate,
16861 extra_columns: extra_columns.clone(),
16862 extra_orders: extra_orders.clone(),
16863 expression,
16864 is_unique,
16865 opclass,
16866 method_name,
16867 }))
16868 }
16869
16870 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16871 /// column-level `REFERENCES ...` clause. The trailing FK is
16872 /// normalised into table-level shape (single-element columns +
16873 /// parent_columns) so the engine sees one uniform constraint list.
16874 fn parse_column_def_with_fk(
16875 &mut self,
16876 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16877 let col = self.parse_column_def()?;
16878 // v7.39 (round 308, V29) — an explicitly named inline FK:
16879 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16880 // loop leaves this spelling intact precisely so the name can be
16881 // kept here; PG reports it in violation messages and matches it
16882 // in `SET CONSTRAINTS`.
16883 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16884 {
16885 self.advance();
16886 Some(self.expect_ident_like()?)
16887 } else {
16888 None
16889 };
16890 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16891 let inline_references = matches!(
16892 self.peek(),
16893 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16894 );
16895 if !inline_references {
16896 return Ok((col, None));
16897 }
16898 let (
16899 parent_table,
16900 parent_columns,
16901 on_delete,
16902 on_update,
16903 match_type,
16904 deferrable,
16905 initially_deferred,
16906 ) = self.parse_references_tail(1)?;
16907 let fk = ForeignKeyConstraint {
16908 name: declared_name,
16909 columns: vec![col.name.clone()],
16910 parent_table,
16911 parent_columns,
16912 on_delete,
16913 on_update,
16914 match_type,
16915 deferrable,
16916 initially_deferred,
16917 };
16918 Ok((col, Some(fk)))
16919 }
16920
16921 /// v7.13.0 — parse a column type (consuming the type ident and
16922 /// any trailing parameters / `[]`), without surrounding column
16923 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16924 /// Returns the resolved `ColumnTypeName` plus implied
16925 /// `(auto_increment, not_null)` flags from PG SERIAL family
16926 /// shorthands — callers that don't expect those (ALTER COLUMN
16927 /// TYPE) can discard them.
16928 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16929 let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16930 Ok(ty)
16931 }
16932
16933 #[allow(clippy::type_complexity)]
16934 fn parse_type_with_implied_flags(
16935 &mut self,
16936 ) -> Result<
16937 (
16938 ColumnTypeName,
16939 bool,
16940 bool,
16941 Option<String>,
16942 Collation,
16943 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16944 bool,
16945 // v7.39 (round 676) — the collation NAME as written, which the
16946 // `Collation` enum above cannot carry.
16947 Option<String>,
16948 bool,
16949 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16950 // list captured at type-parse time. None for all
16951 // non-ENUM types.
16952 Option<Vec<String>>,
16953 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16954 // list. Distinct from ENUM (subset semantics).
16955 Option<Vec<String>>,
16956 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16957 // width, lost when the type collapses to SmallInt / Int.
16958 Option<MysqlIntWidth>,
16959 // v7.39 (round 424) — declared fractional-seconds precision of a
16960 // MySQL temporal column (bare spelling = 0). None under PG.
16961 Option<u8>,
16962 // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16963 // two are different types on MySQL and SPG stores both as
16964 // `Timestamp`, so the spelling has to travel separately or
16965 // a dump silently rewrites the column.
16966 bool,
16967 // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16968 // display hint: it rounds on write.
16969 Option<(u8, u8)>,
16970 ),
16971 ParseError,
16972 > {
16973 let mut ty_ident = match self.advance() {
16974 Token::Ident(s) => s,
16975 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16976 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16977 // '<span>'` literal grammar. As a column type it lands
16978 // here directly; downstream resolution still uses the
16979 // canonical lowercase string.
16980 Token::Interval => "interval".to_string(),
16981 other => {
16982 return Err(ParseError {
16983 message: format!("expected column type, got {other:?}"),
16984 token_pos: self.consumed_pos(),
16985 });
16986 }
16987 };
16988 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16989 // pg_dump qualifies extension types (`public.vector(1024)`).
16990 // SPG is single-namespace; drop the schema and resolve the
16991 // bare type — same treatment table names already get.
16992 while matches!(self.peek(), Token::Dot) {
16993 self.advance();
16994 ty_ident = self.expect_ident_like()?;
16995 }
16996 let mut implied_auto_increment = false;
16997 let mut implied_not_null = false;
16998 let mut user_type_ref: Option<String> = None;
16999 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17000 // value list, captured here and bubbled up through the
17001 // ColumnDef so the engine can attach it to the column
17002 // schema (and validate INSERT cells against it).
17003 let mut inline_enum_variants: Option<Vec<String>> = None;
17004 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17005 let mut inline_set_variants: Option<Vec<String>> = None;
17006 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17007 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17008 // collapses to SmallInt / Int. Only under the MySQL dialect.
17009 let mut mysql_int_width: Option<MysqlIntWidth> = None;
17010 // v7.39 (round 424) — the declared fractional-seconds precision of a
17011 // MySQL temporal column. Set by the temporal arms below; stays None
17012 // for PG (whose temporal columns keep full microseconds).
17013 let mut mysql_fsp: Option<u8> = None;
17014 let mut mysql_declared_timestamp = false;
17015 let mut mysql_float_md: Option<(u8, u8)> = None;
17016 let mut ty = match ty_ident.as_str() {
17017 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17018 "smallserial" | "serial2" => {
17019 implied_auto_increment = true;
17020 implied_not_null = true;
17021 ColumnTypeName::SmallInt
17022 }
17023 "serial" | "serial4" => {
17024 implied_auto_increment = true;
17025 implied_not_null = true;
17026 ColumnTypeName::Int
17027 }
17028 "bigserial" | "serial8" => {
17029 implied_auto_increment = true;
17030 implied_not_null = true;
17031 ColumnTypeName::BigInt
17032 }
17033 // MySQL flavours we accept by aliasing to the closest SPG
17034 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17035 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17036 // 24-bit) → INT. UNSIGNED modifiers are consumed below
17037 // without semantic effect.
17038 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17039 // PG's internal type names; pg_dump and hand-written PG schemas
17040 // use them interchangeably with smallint / int / bigint (the cast
17041 // path already accepted them, only the column grammar didn't).
17042 "smallint" | "int2" => {
17043 // v7.14.0 — MySQL display-width on integers
17044 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17045 // parenthesised number is purely cosmetic — it
17046 // doesn't change storage. Accept + discard.
17047 self.consume_optional_paren_size();
17048 ColumnTypeName::SmallInt
17049 }
17050 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17051 // canonical encoding for BOOLEAN. Every MySQL driver
17052 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17053 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17054 // 4.3 SPG classified TINYINT(1) as SmallInt, which
17055 // gave the customer i16-shaped values where the app
17056 // expected bool — a Tier-A silent type drift on
17057 // mysqldump restores. Now: `TINYINT(1)` → Bool;
17058 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17059 // stay SmallInt (the legacy width-agnostic path).
17060 "tinyint" => {
17061 let width = self.peek_optional_paren_size_value();
17062 self.consume_optional_paren_size();
17063 if width == Some(1) {
17064 ColumnTypeName::Bool
17065 } else {
17066 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17067 // lost width so the write path can enforce -128..127.
17068 if self.mysql_dialect {
17069 mysql_int_width = Some(MysqlIntWidth::Tiny);
17070 }
17071 ColumnTypeName::SmallInt
17072 }
17073 }
17074 "mediumint" => {
17075 self.consume_optional_paren_size();
17076 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17077 if self.mysql_dialect {
17078 mysql_int_width = Some(MysqlIntWidth::Medium);
17079 }
17080 ColumnTypeName::Int
17081 }
17082 "int" | "integer" | "int4" => {
17083 self.consume_optional_paren_size();
17084 ColumnTypeName::Int
17085 }
17086 "bigint" | "int8" => {
17087 self.consume_optional_paren_size();
17088 ColumnTypeName::BigInt
17089 }
17090 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17091 // (mailrs round-5 G6). Consume the optional `PRECISION`
17092 // tail when the type keyword was `double` / `DOUBLE`.
17093 //
17094 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17095 // FLOAT". `FLOAT(p)` picks the width the way PG does:
17096 // p in 1..=24 is real, 25..=53 is double precision, and
17097 // anything else is an error.
17098 "float" | "double" | "real" => {
17099 if ty_ident.eq_ignore_ascii_case("double")
17100 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17101 {
17102 self.advance();
17103 }
17104 if ty_ident.eq_ignore_ascii_case("real") {
17105 // v7.39 (round 274) — the two dialects genuinely
17106 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17107 // synonym for DOUBLE (8-byte). Round 269 made REAL
17108 // 32-bit globally and thereby narrowed the stored
17109 // precision of every MySQL REAL column.
17110 if self.mysql_dialect {
17111 ColumnTypeName::Float
17112 } else {
17113 ColumnTypeName::Real
17114 }
17115 } else if self.mysql_dialect
17116 && matches!(self.peek(), Token::LParen)
17117 && self.peek_paren_has_comma()
17118 {
17119 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17120 // display form (`FLOAT(10,2)`), which PG has no
17121 // equivalent of. It was `syntax error at or near ","`,
17122 // so the whole CREATE failed.
17123 //
17124 // v7.39.2 — the guard said `float` while the comment
17125 // said both, so `DOUBLE(10,2)` — which every legacy
17126 // MySQL schema uses for money — still failed the
17127 // whole CREATE with `syntax error at or near "("`.
17128 // Measured on 9.7.2: both forms are accepted, and the
17129 // digits are NOT a display hint, they round on write
17130 // (3.14159265358979 into either stores 3.14). The
17131 // rounding is recorded as a residual; accepting the
17132 // syntax and keeping the width is the half this
17133 // change makes.
17134 // v7.39.3 — keep the pair. The digits are not a
17135 // display hint: MySQL 9.7.2 ROUNDS on write and
17136 // refuses a value wider than `m` (errno 1264), so a
17137 // column declared for money held more precision here
17138 // than its schema said.
17139 let (m, d) = self.parse_optional_numeric_params()?;
17140 mysql_float_md = Some((
17141 u8::try_from(m).unwrap_or(u8::MAX),
17142 u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17143 ));
17144 if ty_ident.eq_ignore_ascii_case("float") {
17145 ColumnTypeName::Real
17146 } else {
17147 ColumnTypeName::Float
17148 }
17149 } else if ty_ident.eq_ignore_ascii_case("float")
17150 && matches!(self.peek(), Token::LParen)
17151 {
17152 // PG words the two bounds differently, and
17153 // parse_paren_size already rejects a zero.
17154 let p = self.parse_paren_size("FLOAT")?;
17155 if p > 53 {
17156 return Err(self.err(String::from(
17157 "precision for type float must be less than 54 bits",
17158 )));
17159 }
17160 if p <= 24 {
17161 ColumnTypeName::Real
17162 } else {
17163 ColumnTypeName::Float
17164 }
17165 } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17166 // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17167 // eight (it is `float8`'s spelling there). SPG used
17168 // PG's for both, so a MySQL FLOAT column silently
17169 // kept more precision than MySQL does — measured,
17170 // 3.14159265358979 comes back as 3.14159 there and
17171 // came back whole here — and reported itself as
17172 // `double` to every reflection.
17173 //
17174 // This is the mirror of the REAL split above: the
17175 // two dialects disagree about which spelling means
17176 // which width, and one of them was already honoured.
17177 ColumnTypeName::Real
17178 } else {
17179 ColumnTypeName::Float
17180 }
17181 }
17182 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17183 "float4" => ColumnTypeName::Real,
17184 "float8" => ColumnTypeName::Float,
17185 "text" => ColumnTypeName::Text,
17186 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17187 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17188 // real MySQL schema and NONE of them existed: the CREATE
17189 // failed outright with `type "blob" does not exist`, so the
17190 // table was never made. The sizes differ only in MySQL's
17191 // maximum length, which SPG does not cap, so they collapse
17192 // onto TEXT and BYTEA the way the unsized spellings do.
17193 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17194 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17195 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17196 // enforce, consumed so the declaration parses.
17197 "varbinary" | "binary" => {
17198 self.consume_optional_paren_size();
17199 ColumnTypeName::Bytes
17200 }
17201 "name" => ColumnTypeName::Name,
17202 "xid" => ColumnTypeName::Xid,
17203 "oid" => ColumnTypeName::Oid,
17204 "xid8" => ColumnTypeName::Xid8,
17205 "bool" | "boolean" => ColumnTypeName::Bool,
17206 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17207 // an unbounded `character varying`, which the arm below has always
17208 // read as text. Only the short spelling demanded a length, so
17209 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17210 // there is — failed on `VARCHAR type requires (N)` while the long
17211 // spelling of the same thing was accepted. The same asymmetry
17212 // round 613 closed on the CAST side, here on the DDL side.
17213 "varchar" => {
17214 if matches!(self.peek(), Token::LParen) {
17215 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17216 } else {
17217 ColumnTypeName::Text
17218 }
17219 }
17220 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17221 // `character` below (SQL standard).
17222 "char" => {
17223 if matches!(self.peek(), Token::LParen) {
17224 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17225 } else {
17226 ColumnTypeName::Char(1)
17227 }
17228 }
17229 // pg_dump's canonical spellings: `character varying(n)` = varchar,
17230 // `character(n)` = char, bare `character` = char(1). Unbounded
17231 // `character varying` maps to text.
17232 "character" => {
17233 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17234 self.advance();
17235 if matches!(self.peek(), Token::LParen) {
17236 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17237 } else {
17238 ColumnTypeName::Text
17239 }
17240 } else if matches!(self.peek(), Token::LParen) {
17241 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17242 } else {
17243 ColumnTypeName::Char(1)
17244 }
17245 }
17246 "vector" => {
17247 let dim = self.parse_paren_size("VECTOR")?;
17248 let encoding = self.parse_optional_vector_encoding()?;
17249 ColumnTypeName::Vector { dim, encoding }
17250 }
17251 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17252 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17253 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17254 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17255 // DECIMAL(10,2))` — how nearly every money column is written,
17256 // in either dialect — was a syntax error and the table was
17257 // never created. `FIXED` is MySQL's alias alone, so it is
17258 // taken only in that dialect.
17259 "numeric" | "decimal" | "dec" => {
17260 let (precision, scale) = self.parse_optional_numeric_params()?;
17261 ColumnTypeName::Numeric(precision, scale)
17262 }
17263 "fixed" if self.mysql_dialect => {
17264 let (precision, scale) = self.parse_optional_numeric_params()?;
17265 ColumnTypeName::Numeric(precision, scale)
17266 }
17267 "date" => ColumnTypeName::Date,
17268 // MySQL's `DATETIME` is the same domain as standard
17269 // `TIMESTAMP` — accept both spellings.
17270 "timestamp" | "datetime" => {
17271 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17272 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17273 // TIME ZONE` clause, so consume it first.
17274 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17275 // (it truncates on write and pads on render), so capture it;
17276 // a bare spelling means precision 0 there. PG stores µs always
17277 // and keeps `None`.
17278 let n = self.take_optional_paren_size();
17279 if self.mysql_dialect {
17280 mysql_fsp = Some(n.unwrap_or(0).min(6));
17281 // v7.39.2 — remember WHICH spelling was written.
17282 // MySQL and MariaDB keep `timestamp` and `datetime`
17283 // apart everywhere a client can read the type back,
17284 // and SPG reported `datetime` for both — so a dump
17285 // and reload silently changed the column's declared
17286 // type, and MySQL's TIMESTAMP is not DATETIME (a
17287 // different range, and UTC conversion on the way in
17288 // and out).
17289 mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17290 }
17291 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17292 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17293 // the full form. SPG canonicalises:
17294 // - WITH TIME ZONE → Timestamptz
17295 // - WITHOUT TIME ZONE → Timestamp
17296 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17297 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17298 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17299 {
17300 self.advance(); // WITH
17301 self.advance(); // TIME
17302 self.advance(); // ZONE
17303 ColumnTypeName::Timestamptz
17304 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17305 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17306 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17307 {
17308 self.advance(); // WITHOUT
17309 self.advance(); // TIME
17310 self.advance(); // ZONE
17311 ColumnTypeName::Timestamp
17312 } else {
17313 // A second `(precision)` cannot legally follow, but the
17314 // old grammar tolerated it; keep that tolerance.
17315 self.consume_optional_paren_size();
17316 ColumnTypeName::Timestamp
17317 }
17318 }
17319 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17320 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17321 // only PG-wire OID differs.
17322 "timestamptz" => {
17323 self.consume_optional_paren_size();
17324 ColumnTypeName::Timestamptz
17325 }
17326 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17327 // validation. We accept the JSONB spelling too because
17328 // most PG clients default to it; SPG doesn't distinguish
17329 // the two (no path-operator perf advantage to model).
17330 "json" => ColumnTypeName::Json,
17331 "jsonb" => ColumnTypeName::Jsonb,
17332 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17333 // surface here. Same storage shape; mapping happens at
17334 // the engine side via the ColumnTypeName → DataType
17335 // resolver. Literal forms are handled at coerce_value
17336 // time so the lexer stays untouched.
17337 "bytea" | "bytes" => ColumnTypeName::Bytes,
17338 // v7.17.0 Phase 7 — PG network address types
17339 // v7.17.0 had a Text-backed fallback here for
17340 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17341 // each to a first-class type; the keywords are
17342 // bound below in the ζ-A block.
17343 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17344 // The actual `to_tsvector` / `@@` / `ts_rank` surface
17345 // arrives in v7.12.1+; the type itself loads here so
17346 // mailrs's `scripts/init-schema.sql` runs unmodified.
17347 "tsvector" => ColumnTypeName::TsVector,
17348 "tsquery" => ColumnTypeName::TsQuery,
17349 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17350 // surface for Django / Rails / Hibernate's default
17351 // PK pattern.
17352 "uuid" => ColumnTypeName::Uuid,
17353 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17354 // Storage = three-field {months, days, micros}, catalog
17355 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17356 // line `INTERVAL` was parser-rejected at CREATE TABLE.
17357 "interval" => {
17358 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17359 // SECOND` and an optional `(p)` precision. SPG stores the full
17360 // {months,days,micros}; consume + ignore the qualifier/precision.
17361 while matches!(self.peek(), Token::To)
17362 || matches!(self.peek(), Token::Ident(s) if matches!(
17363 s.to_ascii_lowercase().as_str(),
17364 "year" | "month" | "day" | "hour" | "minute" | "second"
17365 ))
17366 {
17367 self.advance();
17368 }
17369 self.consume_optional_paren_size();
17370 ColumnTypeName::Interval
17371 }
17372 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17373 // i64 microseconds since 00:00:00. Wire OID 1083.
17374 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17375 "time" => {
17376 // v7.39 (round 424) — MySQL TIME carries a semantic
17377 // fractional-seconds precision, bare meaning 0.
17378 let n = self.take_optional_paren_size();
17379 if self.mysql_dialect {
17380 mysql_fsp = Some(n.unwrap_or(0).min(6));
17381 }
17382 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17383 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17384 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17385 {
17386 self.advance();
17387 self.advance();
17388 self.advance();
17389 ColumnTypeName::TimeTz
17390 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17391 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17392 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17393 {
17394 self.advance();
17395 self.advance();
17396 self.advance();
17397 ColumnTypeName::Time
17398 } else {
17399 ColumnTypeName::Time
17400 }
17401 }
17402 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17403 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17404 "year" => ColumnTypeName::Year,
17405 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17406 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17407 "timetz" => ColumnTypeName::TimeTz,
17408 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17409 // Wire OID 790.
17410 "money" => ColumnTypeName::Money,
17411 // v7.17.0 Phase 3.P0-38 — PG range types.
17412 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17413 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17414 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17415 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17416 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17417 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17418 // v7.37.5 δ — PG 14+ multirange keywords.
17419 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17420 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17421 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17422 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17423 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17424 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17425 // v7.37.5 ε — PG geometry scalar keywords.
17426 "point" => ColumnTypeName::Point,
17427 "lseg" => ColumnTypeName::Lseg,
17428 "path" => ColumnTypeName::Path,
17429 "box" => ColumnTypeName::PgBox,
17430 "polygon" => ColumnTypeName::Polygon,
17431 "line" => ColumnTypeName::Line,
17432 "circle" => ColumnTypeName::Circle,
17433 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17434 "inet" => ColumnTypeName::Inet,
17435 "cidr" => ColumnTypeName::Cidr,
17436 "macaddr" => ColumnTypeName::Macaddr,
17437 "macaddr8" => ColumnTypeName::Macaddr8,
17438 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17439 // width in the value, so the optional `(N)` typmod is accepted and
17440 // ignored (the column stores whatever width it's given).
17441 "bit" => {
17442 let varying = matches!(
17443 self.peek(),
17444 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17445 );
17446 if varying {
17447 self.advance();
17448 }
17449 // v7.39 (round 281) — the length used to be parsed and
17450 // dropped, so `bit(3)` accepted a five-bit string.
17451 let n = if matches!(self.peek(), Token::LParen) {
17452 self.parse_paren_size("BIT")?
17453 } else {
17454 0
17455 };
17456 if varying {
17457 ColumnTypeName::BitVarying(n)
17458 } else {
17459 ColumnTypeName::Bit(n)
17460 }
17461 }
17462 "varbit" => {
17463 let n = if matches!(self.peek(), Token::LParen) {
17464 self.parse_paren_size("VARBIT")?
17465 } else {
17466 0
17467 };
17468 ColumnTypeName::BitVarying(n)
17469 }
17470 "xml" => ColumnTypeName::Xml,
17471 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17472 "hstore" => ColumnTypeName::Hstore,
17473 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17474 // `ENUM('a','b','c')`. Storage is TEXT; the value
17475 // list lands on `inline_enum_variants` for the
17476 // engine to validate INSERT cells against. Empty
17477 // value list is a parse error (matches MySQL).
17478 "enum" => {
17479 // Expect the opening `(`.
17480 if !matches!(self.peek(), Token::LParen) {
17481 return Err(self.err(alloc::format!(
17482 "expected '(' after ENUM, got {:?}",
17483 self.peek()
17484 )));
17485 }
17486 self.advance();
17487 let mut variants: Vec<String> = Vec::new();
17488 loop {
17489 match self.advance() {
17490 Token::String(s) => variants.push(s),
17491 other => {
17492 return Err(self.err(alloc::format!(
17493 "ENUM(...) expects string literal variants, got {other:?}"
17494 )));
17495 }
17496 }
17497 match self.peek() {
17498 Token::Comma => {
17499 self.advance();
17500 continue;
17501 }
17502 Token::RParen => {
17503 self.advance();
17504 break;
17505 }
17506 other => {
17507 return Err(self.err(alloc::format!(
17508 "expected ',' or ')' in ENUM(...), got {other:?}"
17509 )));
17510 }
17511 }
17512 }
17513 if variants.is_empty() {
17514 return Err(self.err("ENUM(...) must declare at least one variant".into()));
17515 }
17516 inline_enum_variants = Some(variants);
17517 // Storage is plain TEXT; the variant list lives on
17518 // the ColumnSchema side.
17519 ColumnTypeName::Text
17520 }
17521 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17522 // `SET('a','b','c')`. Same parse shape as ENUM;
17523 // semantics differ (subset rather than pick-one).
17524 "set" => {
17525 if !matches!(self.peek(), Token::LParen) {
17526 return Err(self.err(alloc::format!(
17527 "expected '(' after SET, 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 "SET(...) 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 SET(...), got {other:?}"
17554 )));
17555 }
17556 }
17557 }
17558 if variants.is_empty() {
17559 return Err(self.err("SET(...) must declare at least one variant".into()));
17560 }
17561 inline_set_variants = Some(variants);
17562 ColumnTypeName::Text
17563 }
17564 _other => {
17565 // v7.17.0 Phase 1.4 — unknown ident → defer
17566 // resolution to the engine. Stored as Text in
17567 // ColumnTypeName + the original name carried as
17568 // `user_type_ref` so CREATE TABLE can look up
17569 // user-defined enum / domain types.
17570 user_type_ref = Some(ty_ident.clone());
17571 ColumnTypeName::Text
17572 }
17573 };
17574 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17575 // right after the type keyword. Pre-4.4 SPG consumed +
17576 // discarded the keyword, leaving a customer column
17577 // declared `id INT UNSIGNED NOT NULL` silently accepting
17578 // negative values — a Tier-A correctness drift where
17579 // application invariants (auto-increment-IDs never
17580 // negative) silently broke on cutover. Now: capture as
17581 // a column flag, persist on the schema, enforce at
17582 // INSERT / UPDATE time.
17583 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17584 {
17585 self.advance();
17586 true
17587 } else {
17588 false
17589 };
17590 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17591 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17592 // stores text as UTF-8 always so CHARACTER SET is still a
17593 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17594 // name: it gets classified into a `Collation` variant the
17595 // engine consults at WHERE-eval time. PG `default` /
17596 // `pg_catalog.default` / `C` / `POSIX` collations all
17597 // resolve to `Binary` (the prior behaviour); `_ci` /
17598 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17599 // The schema-qualifier form (`pg_catalog.default`) lexes
17600 // as `Ident '.' Ident` — peek for the `.` and consume both
17601 // halves so it's treated as one collation name. PG's
17602 // `IDENT.IDENT` collation form (which can appear here) is
17603 // resolved by Collation::from_collation_name on the bare
17604 // identifier after the dot.
17605 let mut collation = Collation::Binary;
17606 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17607 // clause was written. The engine needs this to tell an explicit
17608 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17609 // clause at all: both resolve to `Collation::Binary`, but under the
17610 // MySQL dialect the latter takes the folding default collation.
17611 let mut collation_explicit = false;
17612 let mut collation_name: Option<alloc::string::String> = None;
17613 loop {
17614 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17615 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17616 {
17617 self.advance(); // CHARACTER
17618 self.advance(); // SET
17619 if matches!(
17620 self.peek(),
17621 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17622 ) {
17623 self.advance();
17624 }
17625 continue;
17626 }
17627 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17628 self.advance(); // COLLATE
17629 // Accept Ident / QuotedIdent / String AND the
17630 // keyword-tokenised `Default` (PG `pg_catalog.default`
17631 // and bare `DEFAULT` collation names — `default` is a
17632 // reserved word so the lexer hands back Token::Default
17633 // not Token::Ident).
17634 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17635 match this.peek().clone() {
17636 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17637 this.advance();
17638 Some(s)
17639 }
17640 Token::Default => {
17641 this.advance();
17642 Some(alloc::string::String::from("default"))
17643 }
17644 _ => None,
17645 }
17646 };
17647 let raw = if let Some(head) = read_collation_atom(self) {
17648 // Schema-qualified PG form: `pg_catalog.default`.
17649 if matches!(self.peek(), Token::Dot) {
17650 self.advance();
17651 let tail = read_collation_atom(self).unwrap_or_default();
17652 alloc::format!("{head}.{tail}")
17653 } else {
17654 head
17655 }
17656 } else {
17657 alloc::string::String::new()
17658 };
17659 if !raw.is_empty() {
17660 collation_explicit = true;
17661 // v7.39 (round 676) — keep the name too. The enum below
17662 // folds C / POSIX / en_US / default into one value, and
17663 // `pg_attribute.attcollation` has to tell them apart.
17664 // The schema qualifier goes: PG's `pg_catalog.default`
17665 // and a bare `default` name the same collation.
17666 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17667 // encoding suffix. Round 676 used `rsplit('.')` for
17668 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17669 // PG writes `pg_catalog.default` (qualifier) and
17670 // `en_US.utf8` (locale + encoding) with the same
17671 // separator. Only `pg_catalog.` is a qualifier, and it
17672 // is the only one PG's own dumps emit.
17673 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17674 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17675 collation_name = Some(alloc::string::String::from(bare));
17676 let parsed = Collation::from_collation_name(&raw);
17677 // Last COLLATE clause wins, but `Binary` from a
17678 // bare keyword like `default` should not
17679 // silently downgrade a stronger one set earlier
17680 // on the same column. v7.17 only ships one
17681 // non-Binary variant so a simple OR is enough.
17682 if parsed != Collation::Binary {
17683 collation = parsed;
17684 }
17685 }
17686 continue;
17687 }
17688 break;
17689 }
17690 // v7.10.10 — postfix `[]` widens the base type to its array
17691 // type. PG accepts `TYPE[]` after any base type and so does
17692 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17693 // all through; the old "only TEXT[]" note was stale).
17694 if matches!(self.peek(), Token::LBracket) {
17695 self.advance();
17696 if !matches!(self.peek(), Token::RBracket) {
17697 return Err(self.err(alloc::format!(
17698 "TEXT[] takes no dimension; got {:?}",
17699 self.peek()
17700 )));
17701 }
17702 self.advance();
17703 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17704 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17705 // still error here.
17706 ty = match ty {
17707 ColumnTypeName::Text => ColumnTypeName::TextArray,
17708 ColumnTypeName::Int => ColumnTypeName::IntArray,
17709 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17710 // v7.40.0 — `oid[]`. Everything downstream of the
17711 // parser already handled `DataType::OidArray`; this
17712 // arm is the whole of what was missing.
17713 ColumnTypeName::Oid => ColumnTypeName::OidArray,
17714 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17715 // `[]` grammar. Wire OID 1187.
17716 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17717 // v7.37.5 γ — full PG array-of-scalar family.
17718 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17719 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17720 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17721 // NUMERIC(p, s) loses its precision params at the
17722 // array level (matches PG: `NUMERIC[]` is untyped,
17723 // per-element precision flows through values).
17724 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17725 ColumnTypeName::Date => ColumnTypeName::DateArray,
17726 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17727 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17728 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17729 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17730 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17731 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17732 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17733 // the array level (matches PG semantics where the
17734 // element precision is per-row, not column-wide).
17735 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17736 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17737 // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17738 // precision the same way NUMERIC[] does.
17739 ColumnTypeName::Real => ColumnTypeName::RealArray,
17740 ColumnTypeName::Time => ColumnTypeName::TimeArray,
17741 ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17742 ColumnTypeName::Inet => ColumnTypeName::InetArray,
17743 ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17744 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17745 // follow-up.
17746 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17747 other => {
17748 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17749 }
17750 };
17751 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17752 // for INT/TEXT/BIGINT. Anything else is an error.
17753 if matches!(self.peek(), Token::LBracket) {
17754 self.advance();
17755 if !matches!(self.peek(), Token::RBracket) {
17756 return Err(self.err(alloc::format!(
17757 "TYPE[][] second dimension takes no size; got {:?}",
17758 self.peek()
17759 )));
17760 }
17761 self.advance();
17762 ty = match ty {
17763 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17764 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17765 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17766 // v7.39 (read01 round 75) — bool[][].
17767 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17768 other => {
17769 return Err(self.err(alloc::format!(
17770 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17771 TEXT[][] only; got {other:?}"
17772 )));
17773 }
17774 };
17775 }
17776 }
17777 Ok((
17778 ty,
17779 implied_auto_increment,
17780 implied_not_null,
17781 user_type_ref,
17782 collation,
17783 collation_explicit,
17784 collation_name,
17785 is_unsigned,
17786 inline_enum_variants,
17787 inline_set_variants,
17788 mysql_int_width,
17789 mysql_fsp,
17790 mysql_declared_timestamp,
17791 mysql_float_md,
17792 ))
17793 }
17794
17795 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17796 // v7.20 — PG reserves the table-constraint keywords, so a
17797 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17798 // malformed constraint clause (e.g. `UNIQUE a` missing its
17799 // parens), not a column named "unique". Since v7.17's
17800 // unknown-type leniency (`user_type_ref`) such a clause
17801 // would otherwise parse as a column with a user-defined
17802 // type — silently accepting invalid DDL. Quoted
17803 // identifiers ("unique" / `unique`) remain valid names.
17804 if let Token::Ident(s) = self.peek()
17805 && [
17806 "unique",
17807 "primary",
17808 "foreign",
17809 "constraint",
17810 "check",
17811 "references",
17812 "exclude",
17813 ]
17814 .iter()
17815 .any(|kw| s.eq_ignore_ascii_case(kw))
17816 {
17817 return Err(self.err(alloc::format!(
17818 "unexpected reserved keyword '{s}' at start of column definition \
17819 (malformed table constraint?)"
17820 )));
17821 }
17822 let name_tok = self.pos;
17823 let name = self.expect_ident_like()?;
17824 // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17825 // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17826 // information_schema, and in SHOW CREATE (measured). SPG folded
17827 // an unquoted name, so a table restored from a dump reported
17828 // names the application had never written.
17829 //
17830 // The written form comes back from the source span, which only
17831 // the MySQL dialect keeps. The span runs to the START of the
17832 // next token, so a comment or unusual spacing between them
17833 // arrives with it — hence the check that what came back is the
17834 // same identifier. It is not decoration: without it,
17835 // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17836 // `MyCol /* c */`.
17837 let name = self
17838 .source_span(name_tok, name_tok)
17839 .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17840 .filter(|raw| raw.eq_ignore_ascii_case(&name))
17841 .map_or(name, alloc::string::String::from);
17842 let (
17843 ty,
17844 implied_auto_increment,
17845 implied_not_null,
17846 user_type_ref,
17847 collation,
17848 collation_explicit,
17849 collation_name,
17850 is_unsigned,
17851 inline_enum_variants,
17852 inline_set_variants,
17853 mysql_int_width,
17854 mysql_fsp,
17855 mysql_declared_timestamp,
17856 mysql_float_md,
17857 ) = self.parse_type_with_implied_flags()?;
17858 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17859 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17860 // each at most once.
17861 let mut default: Option<Expr> = None;
17862 let mut nullable = !implied_not_null;
17863 let mut nullability_seen = implied_not_null;
17864 let mut auto_increment = implied_auto_increment;
17865 let mut is_primary_key = false;
17866 let mut is_unique = false;
17867 let mut unique_nulls_not_distinct = false;
17868 let mut constraint_deferrable = false;
17869 let mut constraint_initially_deferred = false;
17870 let mut check: Option<Expr> = None;
17871 let mut on_update_runtime: Option<Expr> = None;
17872 let mut generated_stored_expr: Option<Box<Expr>> = None;
17873 let mut identity_always = false;
17874 loop {
17875 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17876 // not-null constraints by name and pg_dump emits them
17877 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17878 // NOT NULL`. Accept and discard the name; whatever
17879 // constraint follows is parsed by the arms below.
17880 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17881 // v7.39 (round 308, V29) — a name on an inline
17882 // REFERENCES belongs to the FOREIGN KEY, and the caller
17883 // (`parse_column_def_with_fk`) is what builds it, so
17884 // leave the whole clause for it. Dropping the name here
17885 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17886 // as the synthesised `c_pid_fkey` — which then could
17887 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17888 // `advance()` takes tokens by `mem::replace`, so there
17889 // is no rewinding once consumed.
17890 if matches!(
17891 self.tokens.get(self.pos + 2),
17892 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17893 ) {
17894 break;
17895 }
17896 self.advance();
17897 let _name = self.expect_ident_like()?;
17898 continue;
17899 }
17900 // v7.39 (round 379) — MySQL's SHORT generated-column form
17901 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17902 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17903 // below), but hand-written schemas and app migrations use this.
17904 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17905 // SPG computes-and-stores either way, like the long form.
17906 if matches!(self.peek(), Token::As) {
17907 self.advance();
17908 if !matches!(self.peek(), Token::LParen) {
17909 return Err(self.err(alloc::format!(
17910 "expected '(' after AS in a generated column, got {:?}",
17911 self.peek()
17912 )));
17913 }
17914 self.advance();
17915 let expr = self.parse_expr(0)?;
17916 if !matches!(self.peek(), Token::RParen) {
17917 return Err(self.err(alloc::format!(
17918 "expected ')' after AS (<expr>), got {:?}",
17919 self.peek()
17920 )));
17921 }
17922 self.advance();
17923 if matches!(self.peek(), Token::Ident(s)
17924 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17925 {
17926 self.advance();
17927 }
17928 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17929 continue;
17930 }
17931 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17932 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17933 // the modern replacement for SERIAL in hand-written
17934 // schemas). Both flavours map onto the auto-increment
17935 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17936 // ALWAYS's reject-explicit-values nuance is documented
17937 // leniency. Generated EXPRESSION columns
17938 // (`AS (expr) STORED`) are not supported: error loudly
17939 // instead of silently storing NULLs.
17940 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17941 self.advance();
17942 let mut saw_generated_always = false;
17943 match self.peek().clone() {
17944 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17945 self.advance();
17946 saw_generated_always = true;
17947 }
17948 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17949 self.advance();
17950 if !matches!(self.peek(), Token::Default) {
17951 return Err(self.err(alloc::format!(
17952 "expected DEFAULT after GENERATED BY, got {:?}",
17953 self.peek()
17954 )));
17955 }
17956 self.advance();
17957 }
17958 other => {
17959 return Err(self.err(alloc::format!(
17960 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17961 )));
17962 }
17963 }
17964 if !matches!(self.peek(), Token::As) {
17965 return Err(self.err(alloc::format!(
17966 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17967 self.peek()
17968 )));
17969 }
17970 self.advance();
17971 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17972 // ( <expr> ) STORED` stored computed-column. The
17973 // expression is captured for the engine to recompute
17974 // on every INSERT / UPDATE. v7.37.7 accepts the
17975 // STORED keyword only; PG also has VIRTUAL, which
17976 // v7.37.7 carves out (sentori only uses STORED).
17977 if matches!(self.peek(), Token::LParen) {
17978 self.advance();
17979 let expr = self.parse_expr(0)?;
17980 if !matches!(self.peek(), Token::RParen) {
17981 return Err(self.err(alloc::format!(
17982 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17983 self.peek()
17984 )));
17985 }
17986 self.advance();
17987 let stored = match self.peek() {
17988 Token::Ident(s) | Token::QuotedIdent(s)
17989 if s.eq_ignore_ascii_case("stored") =>
17990 {
17991 self.advance();
17992 true
17993 }
17994 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17995 // generated columns. SPG computes them on write and
17996 // persists like STORED; the two are observably
17997 // identical for query results (the value, recompute
17998 // on base-column change, and NOT NULL enforcement all
17999 // match), so a PG 18 schema/dump using VIRTUAL loads
18000 // and behaves correctly. The compute-on-read storage
18001 // saving is an invisible internal difference.
18002 Token::Ident(s) | Token::QuotedIdent(s)
18003 if s.eq_ignore_ascii_case("virtual") =>
18004 {
18005 self.advance();
18006 false
18007 }
18008 other => {
18009 return Err(self.err(alloc::format!(
18010 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18011 got {other:?}"
18012 )));
18013 }
18014 };
18015 let _ = stored; // STORED / VIRTUAL both compute-and-store.
18016 generated_stored_expr = Some(Box::new(expr));
18017 continue;
18018 }
18019 self.expect_keyword_ident("identity")?;
18020 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18021 // consume the balanced parens and discard (SPG's
18022 // auto-increment is max+1-scan based).
18023 if matches!(self.peek(), Token::LParen) {
18024 let mut depth = 0usize;
18025 loop {
18026 match self.advance() {
18027 Token::LParen => depth += 1,
18028 Token::RParen => {
18029 depth -= 1;
18030 if depth == 0 {
18031 break;
18032 }
18033 }
18034 Token::Eof => {
18035 return Err(self.err(
18036 "unterminated sequence-options parens after IDENTITY".into(),
18037 ));
18038 }
18039 _ => {}
18040 }
18041 }
18042 }
18043 auto_increment = true;
18044 // v7.38 (read01) — remember the ALWAYS flavour so the engine
18045 // can reject explicit non-DEFAULT INSERT values (unless
18046 // OVERRIDING SYSTEM VALUE) the way PG does.
18047 identity_always = saw_generated_always;
18048 // PG identity columns are implicitly NOT NULL.
18049 nullable = false;
18050 continue;
18051 }
18052 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18053 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18054 // is accepted today. The "ON" token is an Ident
18055 // (not reserved) — peek before consuming.
18056 if matches!(self.peek(), Token::On)
18057 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18058 {
18059 self.advance(); // ON
18060 self.advance(); // update
18061 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18062 let next = self.peek().clone();
18063 match next {
18064 Token::Ident(s) | Token::QuotedIdent(s)
18065 if s.eq_ignore_ascii_case("current_timestamp") =>
18066 {
18067 self.advance();
18068 // Optional `(N)` precision.
18069 if matches!(self.peek(), Token::LParen) {
18070 self.advance();
18071 if !matches!(self.peek(), Token::Integer(_)) {
18072 return Err(self.err(alloc::format!(
18073 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18074 self.peek()
18075 )));
18076 }
18077 self.advance();
18078 if !matches!(self.peek(), Token::RParen) {
18079 return Err(self.err(alloc::format!(
18080 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18081 self.peek()
18082 )));
18083 }
18084 self.advance();
18085 }
18086 on_update_runtime = Some(Expr::FunctionCall {
18087 name: "now".into(),
18088 args: Vec::new(),
18089 });
18090 continue;
18091 }
18092 other => {
18093 return Err(self.err(alloc::format!(
18094 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18095 )));
18096 }
18097 }
18098 }
18099 if matches!(self.peek(), Token::Default) {
18100 if default.is_some() {
18101 return Err(self.err("DEFAULT specified twice".into()));
18102 }
18103 self.advance();
18104 default = Some(self.parse_expr(0)?);
18105 continue;
18106 }
18107 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18108 // token with NOT NULL and sits EARLIER in the loop than the
18109 // deferrability arm, so without the lookahead it was reported as
18110 // "NOT NULL specified twice" (or "expected NULL after NOT").
18111 if matches!(self.peek(), Token::Not)
18112 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18113 {
18114 // NOT DEFERRABLE — explicit immediate; nothing to carry.
18115 self.consume_optional_deferrable_clauses()?;
18116 continue;
18117 }
18118 if matches!(self.peek(), Token::Not) {
18119 if nullability_seen {
18120 return Err(self.err("NOT NULL specified twice".into()));
18121 }
18122 self.advance();
18123 if !matches!(self.peek(), Token::Null) {
18124 return Err(self.err(format!(
18125 "expected NULL after NOT in column def, got {:?}",
18126 self.peek()
18127 )));
18128 }
18129 self.advance();
18130 nullable = false;
18131 nullability_seen = true;
18132 continue;
18133 }
18134 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18135 // "this column is nullable" marker (the default in
18136 // standard SQL anyway). mysqldump emits it routinely
18137 // (`col TYPE NULL DEFAULT NULL` for nullable
18138 // timestamps etc). Accept + no-op.
18139 if matches!(self.peek(), Token::Null) {
18140 if nullability_seen && !nullable {
18141 // v7.39 (round 761, F31 tranche 2 #31) — PG's
18142 // sentence, PG18-measured (the table name is the
18143 // caller's; the column half is exact).
18144 return Err(self.err(alloc::format!(
18145 "conflicting NULL/NOT NULL declarations for column \"{name}\""
18146 )));
18147 }
18148 self.advance();
18149 nullable = true;
18150 nullability_seen = true;
18151 continue;
18152 }
18153 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18154 // arrives as a bare Ident. Match either, case-insensitive.
18155 if let Token::Ident(s) = self.peek()
18156 && (s.eq_ignore_ascii_case("auto_increment")
18157 || s.eq_ignore_ascii_case("autoincrement"))
18158 {
18159 if auto_increment {
18160 return Err(self.err("AUTO_INCREMENT specified twice".into()));
18161 }
18162 self.advance();
18163 auto_increment = true;
18164 continue;
18165 }
18166 // v7.9.13 — inline `PRIMARY KEY` column constraint
18167 // (mailrs F1). Implies `NOT NULL`. The engine creates
18168 // a BTree index for the PK column at CREATE TABLE time
18169 // so FK parent-side index lookups resolve.
18170 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18171 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18172 // spelling was a parse error, so a pg_dump carrying one stopped
18173 // mid-restore. The clauses are consumed by the same helper the FK
18174 // path has used since round 288 and recorded nowhere: SPG enforces
18175 // the constraint IMMEDIATELY either way, which fails earlier than
18176 // PG inside a transaction that violates-then-repairs — a refusal,
18177 // not a wrong answer. True deferral is the open remainder of F08.
18178 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18179 || (matches!(self.peek(), Token::Not)
18180 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18181 {
18182 // v7.39 (round 711) — CARRIED now (the storing half of
18183 // F08); round 621 only consumed.
18184 let (d, idef) = self.consume_deferrable_clauses_timed()?;
18185 constraint_deferrable |= d;
18186 constraint_initially_deferred |= idef;
18187 continue;
18188 }
18189 if let Token::Ident(s) = self.peek()
18190 && s.eq_ignore_ascii_case("primary")
18191 {
18192 if is_primary_key {
18193 return Err(self.err("PRIMARY KEY specified twice".into()));
18194 }
18195 // Peek-ahead for the required `KEY` token.
18196 let next = self.tokens.get(self.pos + 1);
18197 let next_is_key = matches!(
18198 next,
18199 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18200 );
18201 if !next_is_key {
18202 return Err(self.err(format!(
18203 "expected KEY after PRIMARY in column def, got {:?}",
18204 next
18205 )));
18206 }
18207 self.advance(); // PRIMARY
18208 self.advance(); // KEY
18209 is_primary_key = true;
18210 if nullability_seen && nullable {
18211 return Err(self.err(
18212 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18213 ));
18214 }
18215 nullable = false;
18216 nullability_seen = true;
18217 continue;
18218 }
18219 // v7.13.0 — inline `UNIQUE` column constraint
18220 // (mailrs round-5 G2). Fold into a single-column
18221 // table-level UNIQUE at CREATE TABLE post-process time.
18222 if let Token::Ident(s) = self.peek()
18223 && s.eq_ignore_ascii_case("unique")
18224 {
18225 if is_unique {
18226 return Err(self.err("UNIQUE specified twice".into()));
18227 }
18228 self.advance();
18229 is_unique = true;
18230 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18231 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18232 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18233 let n1 = self.tokens.get(self.pos + 1);
18234 let n2 = self.tokens.get(self.pos + 2);
18235 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18236 self.advance(); // NULLS
18237 self.advance(); // NOT
18238 self.advance(); // DISTINCT
18239 unique_nulls_not_distinct = true;
18240 } else if matches!(n1, Some(Token::Distinct)) {
18241 self.advance(); // NULLS
18242 self.advance(); // DISTINCT
18243 }
18244 }
18245 continue;
18246 }
18247 // v7.13.0 — inline `CHECK (<expr>)` column constraint
18248 // (mailrs round-5 G3). PG semantics: column-level
18249 // CHECK is equivalent to a table-level CHECK. Multiple
18250 // inline CHECKs on the same column AND together.
18251 if let Token::Ident(s) = self.peek()
18252 && s.eq_ignore_ascii_case("check")
18253 {
18254 self.advance();
18255 if !matches!(self.peek(), Token::LParen) {
18256 return Err(self.err(alloc::format!(
18257 "expected '(' after CHECK in column def, got {:?}",
18258 self.peek()
18259 )));
18260 }
18261 self.advance();
18262 let pred = self.parse_expr(0)?;
18263 if !matches!(self.peek(), Token::RParen) {
18264 return Err(self.err(alloc::format!(
18265 "expected ')' to close CHECK predicate, got {:?}",
18266 self.peek()
18267 )));
18268 }
18269 self.advance();
18270 check = Some(match check.take() {
18271 Some(prev) => Expr::Binary {
18272 op: BinOp::And,
18273 lhs: Box::new(prev),
18274 rhs: Box::new(pred),
18275 },
18276 None => pred,
18277 });
18278 continue;
18279 }
18280 break;
18281 }
18282 Ok(ColumnDef {
18283 name,
18284 ty,
18285 nullable,
18286 default,
18287 auto_increment,
18288 is_primary_key,
18289 is_unique,
18290 unique_nulls_not_distinct,
18291 constraint_deferrable,
18292 constraint_initially_deferred,
18293 check,
18294 user_type_ref,
18295 on_update_runtime,
18296 collation,
18297 collation_explicit,
18298 collation_name,
18299 is_unsigned,
18300 inline_enum_variants,
18301 inline_set_variants,
18302 generated_stored_expr,
18303 identity_always,
18304 mysql_int_width,
18305 mysql_fsp,
18306 mysql_declared_timestamp,
18307 mysql_float_md,
18308 })
18309 }
18310
18311 /// `NUMERIC` may appear without parameters, with one (precision
18312 /// only, scale=0), or with both. Returns `(precision, scale)` with
18313 /// 0 = unspecified for the bare form.
18314 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18315 if !matches!(self.peek(), Token::LParen) {
18316 // Bare `NUMERIC` — PG treats this as "unlimited precision";
18317 // we surface it as precision=0 to mean "unconstrained" so
18318 // the engine doesn't need a separate variant.
18319 return Ok((0, 0));
18320 }
18321 self.advance();
18322 // v7.39 (round 272) — PG's declared precision runs to 1000, and
18323 // it words the out-of-range case with the value it saw. SPG
18324 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18325 // accepts failed to parse at all; values wider than i128 are
18326 // carried by the arbitrary-precision form.
18327 let precision = match self.advance() {
18328 Token::Integer(n) if (1..=1000).contains(&n) => {
18329 u16::try_from(n).expect("range-checked")
18330 }
18331 Token::Integer(n) => {
18332 return Err(ParseError {
18333 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18334 token_pos: self.consumed_pos(),
18335 });
18336 }
18337 other => {
18338 return Err(ParseError {
18339 message: format!(
18340 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18341 ),
18342 token_pos: self.consumed_pos(),
18343 });
18344 }
18345 };
18346 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18347 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18348 // then overflows). A negative scale rounds to tens / hundreds / …
18349 let scale = if matches!(self.peek(), Token::Comma) {
18350 self.advance();
18351 let neg = if matches!(self.peek(), Token::Minus) {
18352 self.advance();
18353 true
18354 } else {
18355 false
18356 };
18357 match self.advance() {
18358 Token::Integer(n) => {
18359 let signed = if neg { -n } else { n };
18360 if !(-1000..=1000).contains(&signed) {
18361 return Err(ParseError {
18362 message: format!(
18363 "NUMERIC scale {signed} must be between -1000 and 1000"
18364 ),
18365 token_pos: self.consumed_pos(),
18366 });
18367 }
18368 i16::try_from(signed).expect("range-checked")
18369 }
18370 other => {
18371 return Err(ParseError {
18372 message: format!("NUMERIC scale must be an integer, got {other:?}"),
18373 token_pos: self.consumed_pos(),
18374 });
18375 }
18376 }
18377 } else {
18378 0
18379 };
18380 if !matches!(self.peek(), Token::RParen) {
18381 return Err(self.err(format!(
18382 "expected ')' to close NUMERIC params, got {:?}",
18383 self.peek()
18384 )));
18385 }
18386 self.advance();
18387 Ok((precision, scale))
18388 }
18389
18390 /// Parse `(N)` where `N` is a positive integer literal — used by the
18391 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18392 /// for the error message.
18393 /// v6.0.1: parse the optional `USING <encoding>` clause that
18394 /// follows `VECTOR(N)` in a column definition. Missing clause
18395 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18396 /// ident → `ParseError` listing the encodings recognised today.
18397 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18398 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18399 return Ok(VecEncoding::F32);
18400 }
18401 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18402 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18403 // consume the token when the very next token is a known
18404 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18405 // `USING` for the caller — it's the rewrite-expression form.
18406 let n1 = self.tokens.get(self.pos + 1);
18407 let next_is_encoding = matches!(
18408 n1,
18409 Some(Token::Ident(s))
18410 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18411 );
18412 if !next_is_encoding {
18413 return Ok(VecEncoding::F32);
18414 }
18415 self.advance();
18416 let enc_ident = match self.advance() {
18417 Token::Ident(s) => s,
18418 other => {
18419 return Err(self.err(format!(
18420 "expected vector encoding after USING, got {other:?}"
18421 )));
18422 }
18423 };
18424 match enc_ident.to_ascii_lowercase().as_str() {
18425 "sq8" => Ok(VecEncoding::Sq8),
18426 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18427 // binary16 per-element storage.
18428 "half" => Ok(VecEncoding::F16),
18429 other => Err(self.err(format!(
18430 "unknown vector encoding {other:?}; supported: SQ8, HALF"
18431 ))),
18432 }
18433 }
18434
18435 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18436 /// without consuming it. Returns `Some(N)` when the next
18437 /// tokens are `( <int> )`; None otherwise. Used by the
18438 /// TINYINT classifier to decide whether to map to Bool or
18439 /// SmallInt.
18440 fn peek_optional_paren_size_value(&self) -> Option<i64> {
18441 if !matches!(self.peek(), Token::LParen) {
18442 return None;
18443 }
18444 let next = self.tokens.get(self.pos + 1)?;
18445 let n = match next {
18446 Token::Integer(n) => *n,
18447 _ => return None,
18448 };
18449 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18450 return None;
18451 }
18452 Some(n)
18453 }
18454
18455 /// v7.14.0 — consume an optional MySQL display-width
18456 /// parenthesised number after an integer type, returning
18457 /// nothing. `TINYINT(1)` etc.
18458 /// v7.39 (round 360) — does the parenthesised group ahead contain a
18459 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18460 fn peek_paren_has_comma(&self) -> bool {
18461 let mut i = self.pos + 1;
18462 let mut depth = 1usize;
18463 while depth > 0 {
18464 match self.tokens.get(i) {
18465 Some(Token::LParen) => depth += 1,
18466 Some(Token::RParen) => depth -= 1,
18467 Some(Token::Comma) if depth == 1 => return true,
18468 None | Some(Token::Eof) => return false,
18469 _ => {}
18470 }
18471 i += 1;
18472 }
18473 false
18474 }
18475
18476 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18477 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18478 /// fractional-seconds precision that drives write truncation and render
18479 /// padding, where `consume_optional_paren_size` throws it away.
18480 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18481 fn take_optional_paren_size(&mut self) -> Option<u8> {
18482 let Some(Token::Integer(n)) = self
18483 .tokens
18484 .get(self.pos + 1)
18485 .filter(|_| matches!(self.peek(), Token::LParen))
18486 .cloned()
18487 else {
18488 self.consume_optional_paren_size();
18489 return None;
18490 };
18491 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18492 self.consume_optional_paren_size();
18493 return None;
18494 }
18495 self.consume_optional_paren_size();
18496 u8::try_from(n).ok()
18497 }
18498
18499 fn consume_optional_paren_size(&mut self) {
18500 if !matches!(self.peek(), Token::LParen) {
18501 return;
18502 }
18503 self.advance();
18504 // Skip until matching RParen (allow nested or any tokens).
18505 let mut depth = 1usize;
18506 while depth > 0 {
18507 match self.peek() {
18508 Token::LParen => depth += 1,
18509 Token::RParen => depth -= 1,
18510 Token::Eof => return,
18511 _ => {}
18512 }
18513 self.advance();
18514 }
18515 }
18516
18517 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18518 if !matches!(self.peek(), Token::LParen) {
18519 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18520 }
18521 self.advance();
18522 let n = match self.advance() {
18523 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18524 message: format!("{label} size too large: {n}"),
18525 token_pos: self.consumed_pos(),
18526 })?,
18527 other => {
18528 return Err(ParseError {
18529 message: format!("expected positive integer {label} size, got {other:?}"),
18530 token_pos: self.consumed_pos(),
18531 });
18532 }
18533 };
18534 if !matches!(self.peek(), Token::RParen) {
18535 return Err(self.err(format!(
18536 "expected ')' after {label} size, got {:?}",
18537 self.peek()
18538 )));
18539 }
18540 self.advance();
18541 Ok(n)
18542 }
18543
18544 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18545 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18546 /// key, like MySQL) whose action skips conflicting rows.
18547 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18548 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18549 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18550 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18551 /// common bulk-upsert spellings —
18552 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18553 /// REPLACE INTO t SELECT …
18554 /// — were a parse error / a duplicate-key failure respectively.
18555 ///
18556 /// Precedence: an explicitly written clause beats a statement-level flag.
18557 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18558 /// implicit `REPLACE` and `IGNORE` lowerings.
18559 fn parse_insert_conflict_clause(
18560 &mut self,
18561 replace: bool,
18562 ignore: bool,
18563 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18564 if let Some(c) = self.parse_optional_on_duplicate_key()? {
18565 return Ok(Some(c));
18566 }
18567 if let Some(c) = self.parse_optional_on_conflict()? {
18568 return Ok(Some(c));
18569 }
18570 if replace {
18571 // REPLACE INTO = delete-then-insert, which PG spells as
18572 // `ON CONFLICT DO UPDATE SET` over every column; the engine
18573 // reads an empty assignment list as "take the incoming row".
18574 return Ok(Some(crate::ast::OnConflictClause {
18575 target_columns: Vec::new(),
18576 index_where: None,
18577 constraint_name: None,
18578 mysql_lowered: true,
18579 action: crate::ast::OnConflictAction::Update {
18580 assignments: Vec::new(),
18581 where_: None,
18582 },
18583 }));
18584 }
18585 if ignore {
18586 return Ok(Some(Self::insert_ignore_clause()));
18587 }
18588 Ok(None)
18589 }
18590
18591 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18592 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18593 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18594 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18595 fn parse_optional_on_duplicate_key(
18596 &mut self,
18597 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18598 if !(matches!(self.peek(), Token::On)
18599 && matches!(self.tokens.get(self.pos + 1),
18600 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18601 {
18602 return Ok(None);
18603 }
18604 self.advance(); // ON
18605 self.advance(); // DUPLICATE
18606 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18607 return Err(self.err(format!(
18608 "expected KEY after ON DUPLICATE, got {:?}",
18609 self.peek()
18610 )));
18611 }
18612 self.advance();
18613 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18614 return Err(self.err(format!(
18615 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18616 self.peek()
18617 )));
18618 }
18619 self.advance();
18620 let mut assignments: Vec<(String, Expr)> = Vec::new();
18621 loop {
18622 let col = self.expect_ident_like()?;
18623 if !matches!(self.peek(), Token::Eq) {
18624 return Err(self.err(format!(
18625 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18626 self.peek()
18627 )));
18628 }
18629 self.advance();
18630 let mut expr = self.parse_expr(0)?;
18631 Self::rewrite_mysql_values_refs(&mut expr);
18632 assignments.push((col, expr));
18633 if matches!(self.peek(), Token::Comma) {
18634 self.advance();
18635 continue;
18636 }
18637 break;
18638 }
18639 Ok(Some(crate::ast::OnConflictClause {
18640 target_columns: Vec::new(),
18641 index_where: None,
18642 constraint_name: None,
18643 mysql_lowered: true,
18644 action: crate::ast::OnConflictAction::Update {
18645 assignments,
18646 where_: None,
18647 },
18648 }))
18649 }
18650
18651 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18652 crate::ast::OnConflictClause {
18653 target_columns: Vec::new(),
18654 index_where: None,
18655 constraint_name: None,
18656 mysql_lowered: true,
18657 action: crate::ast::OnConflictAction::Nothing,
18658 }
18659 }
18660
18661 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18662 debug_assert!(
18663 matches!(self.peek(), Token::Insert)
18664 || (replace
18665 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18666 );
18667 self.advance();
18668 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18669 // would raise a duplicate-key error instead of failing the statement,
18670 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18671 // plain ident to the lexer; only the MySQL dialect accepts it here.
18672 let ignore = self.mysql_dialect
18673 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18674 if ignore {
18675 self.advance();
18676 }
18677 if !matches!(self.peek(), Token::Into) {
18678 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18679 }
18680 self.advance();
18681 let table = self.expect_ident_like()?;
18682 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18683 // grammar requires the AS keyword here (a bare identifier would be
18684 // ambiguous with a column list). The alias is what the ON CONFLICT
18685 // DO UPDATE expressions refer to the target row by.
18686 let alias = if matches!(self.peek(), Token::As) {
18687 self.advance();
18688 Some(self.expect_ident_like()?)
18689 } else {
18690 None
18691 };
18692 // v7.39 (round 428) — MySQL's SET-form INSERT:
18693 // INSERT INTO t SET a = 1, b = 'x'
18694 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18695 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18696 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18697 // measured). So it lowers to the column list + one VALUES row and
18698 // rejoins the ordinary path, which already handles every one of
18699 // those. PG has no such spelling, hence the dialect gate.
18700 if self.mysql_dialect
18701 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18702 {
18703 self.advance(); // SET
18704 let mut names = Vec::new();
18705 let mut values = Vec::new();
18706 loop {
18707 names.push(self.expect_ident_like()?);
18708 if !matches!(self.peek(), Token::Eq) {
18709 return Err(self.err(alloc::format!(
18710 "expected '=' in INSERT … SET, got {:?}",
18711 self.peek()
18712 )));
18713 }
18714 self.advance();
18715 // `SET a = DEFAULT` rides the same `__column_default` marker
18716 // the VALUES-row and UPDATE-SET paths use; the INSERT
18717 // executor resolves it against the target column.
18718 if matches!(self.peek(), Token::Default) {
18719 self.advance();
18720 values.push(Expr::FunctionCall {
18721 name: "__column_default".to_string(),
18722 args: Vec::new(),
18723 });
18724 } else {
18725 values.push(self.parse_expr(0)?);
18726 }
18727 if matches!(self.peek(), Token::Comma) {
18728 self.advance();
18729 continue;
18730 }
18731 break;
18732 }
18733 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18734 let returning = self.parse_optional_returning()?;
18735 return Ok(Statement::Insert(InsertStatement {
18736 ctes: Vec::new(),
18737 table,
18738 alias,
18739 columns: Some(names),
18740 rows: alloc::vec![values],
18741 select_source: None,
18742 // MySQL's SET form has no `OVERRIDING …` clause (that is
18743 // PG's identity-column spelling).
18744 overriding: Overriding::None,
18745 mysql_ignore: ignore,
18746 on_conflict,
18747 returning,
18748 }));
18749 }
18750 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18751 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18752 // a parenthesized query source instead (PG select_with_parens:
18753 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18754 // both keywords are reserved in PG, so no column list can start
18755 // with them.
18756 let columns = if matches!(self.peek(), Token::LParen) {
18757 self.advance();
18758 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18759 let select_stmt = if self.peek_is_with_kw() {
18760 self.advance();
18761 self.parse_nested_with_select()?
18762 } else {
18763 match self.parse_select_stmt()? {
18764 Statement::Select(s) => s,
18765 other => {
18766 return Err(self.err(alloc::format!(
18767 "expected SELECT in parenthesized INSERT source, got {other:?}"
18768 )));
18769 }
18770 }
18771 };
18772 if !matches!(self.peek(), Token::RParen) {
18773 return Err(self.err(format!(
18774 "expected ')' after parenthesized INSERT source, got {:?}",
18775 self.peek()
18776 )));
18777 }
18778 self.advance();
18779 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18780 let returning = self.parse_optional_returning()?;
18781 return Ok(Statement::Insert(InsertStatement {
18782 ctes: Vec::new(),
18783 table,
18784 alias: alias.clone(),
18785 columns: None,
18786 rows: Vec::new(),
18787 select_source: Some(Box::new(select_stmt)),
18788 on_conflict,
18789 returning,
18790 overriding: Overriding::None,
18791 mysql_ignore: ignore,
18792 }));
18793 }
18794 let mut names = Vec::new();
18795 loop {
18796 names.push(self.expect_ident_like()?);
18797 match self.peek() {
18798 Token::Comma => {
18799 self.advance();
18800 }
18801 Token::RParen => {
18802 self.advance();
18803 break;
18804 }
18805 other => {
18806 return Err(self.err(format!(
18807 "expected ',' or ')' in INSERT column list, got {other:?}"
18808 )));
18809 }
18810 }
18811 }
18812 Some(names)
18813 } else {
18814 None
18815 };
18816 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18817 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18818 // is captured on the statement so the engine can apply PG's
18819 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18820 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18821 {
18822 self.advance();
18823 let which = self.expect_ident_like()?;
18824 let ov = if which.eq_ignore_ascii_case("system") {
18825 Overriding::System
18826 } else if which.eq_ignore_ascii_case("user") {
18827 Overriding::User
18828 } else {
18829 return Err(self.err(format!(
18830 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18831 )));
18832 };
18833 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18834 return Err(self.err(format!(
18835 "expected VALUE after OVERRIDING {}, got {:?}",
18836 which.to_ascii_uppercase(),
18837 self.peek()
18838 )));
18839 }
18840 self.advance();
18841 ov
18842 } else {
18843 Overriding::None
18844 };
18845 // `INSERT INTO t DEFAULT VALUES` — a single row made
18846 // entirely of column defaults. Lower to the permuted
18847 // column-list path with an empty list: every schema column
18848 // is unmapped, so the engine fills each from its default
18849 // (serials advance, plain defaults evaluate, the rest NULL).
18850 if matches!(self.peek(), Token::Default) {
18851 self.advance();
18852 if !matches!(self.peek(), Token::Values) {
18853 return Err(self.err(format!(
18854 "expected VALUES after DEFAULT in INSERT, got {:?}",
18855 self.peek()
18856 )));
18857 }
18858 self.advance();
18859 if columns.is_some() {
18860 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18861 }
18862 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18863 let returning = self.parse_optional_returning()?;
18864 return Ok(Statement::Insert(InsertStatement {
18865 ctes: Vec::new(),
18866 table,
18867 alias: alias.clone(),
18868 columns: Some(Vec::new()),
18869 rows: alloc::vec![Vec::new()],
18870 select_source: None,
18871 on_conflict,
18872 returning,
18873 overriding,
18874 mysql_ignore: ignore,
18875 }));
18876 }
18877 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18878 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18879 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18880 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18881 // own WITH comes before INSERT).
18882 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18883 let select_stmt = if self.peek_is_with_kw() {
18884 self.advance();
18885 self.parse_nested_with_select()?
18886 } else {
18887 match self.parse_select_stmt()? {
18888 Statement::Select(s) => s,
18889 other => {
18890 return Err(self.err(alloc::format!(
18891 "expected SELECT after INSERT INTO ... target, got {other:?}"
18892 )));
18893 }
18894 }
18895 };
18896 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18897 let returning = self.parse_optional_returning()?;
18898 return Ok(Statement::Insert(InsertStatement {
18899 ctes: Vec::new(),
18900 table,
18901 alias: alias.clone(),
18902 columns,
18903 rows: Vec::new(),
18904 select_source: Some(Box::new(select_stmt)),
18905 on_conflict,
18906 returning,
18907 overriding,
18908 mysql_ignore: ignore,
18909 }));
18910 }
18911 if !matches!(self.peek(), Token::Values) {
18912 return Err(self.err(format!(
18913 "expected VALUES or SELECT after table name, got {:?}",
18914 self.peek()
18915 )));
18916 }
18917 self.advance();
18918 if !matches!(self.peek(), Token::LParen) {
18919 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18920 }
18921 let mut rows = Vec::new();
18922 loop {
18923 // Each iteration consumes one `(expr, expr, …)` tuple.
18924 if !matches!(self.peek(), Token::LParen) {
18925 return Err(self.err(format!(
18926 "expected '(' for next VALUES tuple, got {:?}",
18927 self.peek()
18928 )));
18929 }
18930 self.advance();
18931 let mut tuple = Vec::new();
18932 loop {
18933 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18934 // the column's declared default for that slot. Rides out as the
18935 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18936 // path uses; the INSERT executor resolves it per target column.
18937 if matches!(self.peek(), Token::Default) {
18938 self.advance();
18939 tuple.push(Expr::FunctionCall {
18940 name: "__column_default".to_string(),
18941 args: Vec::new(),
18942 });
18943 } else {
18944 tuple.push(self.parse_expr(0)?);
18945 }
18946 match self.peek() {
18947 Token::Comma => {
18948 self.advance();
18949 }
18950 Token::RParen => {
18951 self.advance();
18952 break;
18953 }
18954 other => {
18955 return Err(self.err(format!(
18956 "expected ',' or ')' in VALUES tuple, got {other:?}"
18957 )));
18958 }
18959 }
18960 }
18961 if tuple.is_empty() {
18962 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18963 }
18964 rows.push(tuple);
18965 // Continue with comma-separated tuples.
18966 if matches!(self.peek(), Token::Comma) {
18967 self.advance();
18968 } else {
18969 break;
18970 }
18971 }
18972 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18973 // to ON CONFLICT DO UPDATE with an empty conflict target
18974 // (the engine picks the table's first unique index, which
18975 // matches MySQL's any-unique-key behaviour for the common
18976 // single-key case). `VALUES(col)` in the assignments is
18977 // MySQL's spelling of EXCLUDED.col.
18978 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18979 let returning = self.parse_optional_returning()?;
18980 Ok(Statement::Insert(InsertStatement {
18981 ctes: Vec::new(),
18982 table,
18983 alias,
18984 columns,
18985 rows,
18986 select_source: None,
18987 on_conflict,
18988 returning,
18989 overriding,
18990 mysql_ignore: ignore,
18991 }))
18992 }
18993
18994 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18995 /// the incoming row's value — exactly PG's EXCLUDED.col.
18996 fn rewrite_mysql_values_refs(e: &mut Expr) {
18997 match e {
18998 Expr::FunctionCall { name, args }
18999 if name.eq_ignore_ascii_case("values")
19000 && args.len() == 1
19001 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19002 {
19003 let Expr::Column(c) = &args[0] else {
19004 unreachable!("guarded above");
19005 };
19006 *e = Expr::Column(crate::ast::ColumnName {
19007 qualifier: Some("EXCLUDED".to_string()),
19008 name: c.name.clone(),
19009 });
19010 }
19011 Expr::FunctionCall { args, .. } => {
19012 for a in args {
19013 Self::rewrite_mysql_values_refs(a);
19014 }
19015 }
19016 Expr::Binary { lhs, rhs, .. } => {
19017 Self::rewrite_mysql_values_refs(lhs);
19018 Self::rewrite_mysql_values_refs(rhs);
19019 }
19020 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19021 Self::rewrite_mysql_values_refs(expr);
19022 }
19023 Expr::Case {
19024 operand,
19025 branches,
19026 else_branch,
19027 } => {
19028 if let Some(op) = operand {
19029 Self::rewrite_mysql_values_refs(op);
19030 }
19031 for (w, t) in branches {
19032 Self::rewrite_mysql_values_refs(w);
19033 Self::rewrite_mysql_values_refs(t);
19034 }
19035 if let Some(el) = else_branch {
19036 Self::rewrite_mysql_values_refs(el);
19037 }
19038 }
19039 _ => {}
19040 }
19041 }
19042
19043 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19044 /// clause sitting between the INSERT body and the trailing
19045 /// RETURNING. All keywords come in as bare idents; `ON` is
19046 /// a reserved Token though.
19047 fn parse_optional_on_conflict(
19048 &mut self,
19049 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19050 if !matches!(self.peek(), Token::On) {
19051 return Ok(None);
19052 }
19053 // Peek further: we want exactly "ON CONFLICT ...". If the
19054 // next ident isn't "conflict", let some other parser handle.
19055 let next_is_conflict = matches!(
19056 self.tokens.get(self.pos + 1),
19057 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19058 );
19059 if !next_is_conflict {
19060 return Ok(None);
19061 }
19062 self.advance(); // ON
19063 self.advance(); // CONFLICT
19064 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19065 // the constraint instead of listing columns (the pg_dump
19066 // form); the engine resolves it.
19067 let mut constraint_name: Option<String> = None;
19068 if matches!(self.peek(), Token::On) {
19069 self.advance(); // ON
19070 match self.advance() {
19071 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19072 }
19073 other => {
19074 return Err(self.err(alloc::format!(
19075 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19076 )));
19077 }
19078 }
19079 constraint_name = Some(self.expect_ident_like()?);
19080 }
19081 // Optional `(col [, col]*)` target list.
19082 let mut target_columns: Vec<String> = Vec::new();
19083 if matches!(self.peek(), Token::LParen) {
19084 self.advance();
19085 loop {
19086 target_columns.push(self.expect_ident_like()?);
19087 match self.peek() {
19088 Token::Comma => {
19089 self.advance();
19090 }
19091 Token::RParen => {
19092 self.advance();
19093 break;
19094 }
19095 other => {
19096 return Err(self.err(alloc::format!(
19097 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19098 )));
19099 }
19100 }
19101 }
19102 }
19103 // v7.39 (round 240) — optional index predicate after the target
19104 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19105 // PARTIAL unique index; SPG's arbiters are full indexes, which
19106 // satisfy any predicate, so it is parsed and carried but not
19107 // consulted (recorded residual: partial-unique-index arbiters).
19108 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19109 self.advance();
19110 Some(self.parse_expr(0)?)
19111 } else {
19112 None
19113 };
19114 // Required `DO`.
19115 match self.advance() {
19116 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19117 other => {
19118 return Err(self.err(alloc::format!(
19119 "expected DO after ON CONFLICT [(…)], got {other:?}"
19120 )));
19121 }
19122 }
19123 // Action: NOTHING | UPDATE SET …
19124 let action = match self.advance() {
19125 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19126 crate::ast::OnConflictAction::Nothing
19127 }
19128 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19129 self.parse_on_conflict_update_action()?
19130 }
19131 other => {
19132 return Err(self.err(alloc::format!(
19133 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19134 )));
19135 }
19136 };
19137 Ok(Some(crate::ast::OnConflictClause {
19138 target_columns,
19139 index_where,
19140 constraint_name,
19141 mysql_lowered: false,
19142 action,
19143 }))
19144 }
19145
19146 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19147 /// `SET col = expr [, …] [WHERE cond]`. Caller already
19148 /// consumed `UPDATE`.
19149 fn parse_on_conflict_update_action(
19150 &mut self,
19151 ) -> Result<crate::ast::OnConflictAction, ParseError> {
19152 // `SET`
19153 match self.advance() {
19154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19155 other => {
19156 return Err(self.err(alloc::format!(
19157 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19158 )));
19159 }
19160 }
19161 let mut assignments: Vec<(String, Expr)> = Vec::new();
19162 loop {
19163 let col = self.expect_ident_like()?;
19164 if !matches!(self.peek(), Token::Eq) {
19165 return Err(self.err(alloc::format!(
19166 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19167 self.peek()
19168 )));
19169 }
19170 self.advance();
19171 let value = self.parse_expr(0)?;
19172 assignments.push((col, value));
19173 if matches!(self.peek(), Token::Comma) {
19174 self.advance();
19175 continue;
19176 }
19177 break;
19178 }
19179 let where_ = if matches!(self.peek(), Token::Where) {
19180 self.advance();
19181 Some(self.parse_expr(0)?)
19182 } else {
19183 None
19184 };
19185 Ok(crate::ast::OnConflictAction::Update {
19186 assignments,
19187 where_,
19188 })
19189 }
19190
19191 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19192 let mut items = Vec::new();
19193 // v7.39 (round 341, V66) — PG's target list may be EMPTY
19194 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19195 // answers one zero-column row per row of t, and a bare `SELECT`
19196 // answers a single zero-column row. SPG required at least one
19197 // item, so both were syntax errors. Recognised by the token that
19198 // follows — nothing that can start an expression appears here.
19199 if self.select_list_is_empty_here() {
19200 return Ok(items);
19201 }
19202 loop {
19203 items.push(self.parse_select_item()?);
19204 if matches!(self.peek(), Token::Comma) {
19205 self.advance();
19206 } else {
19207 break;
19208 }
19209 }
19210 Ok(items)
19211 }
19212
19213 /// Is the target list empty at this point — i.e. does the next token
19214 /// end the SELECT's item list rather than start an item?
19215 fn select_list_is_empty_here(&self) -> bool {
19216 match self.peek() {
19217 Token::From
19218 | Token::Where
19219 | Token::Group
19220 | Token::Having
19221 | Token::Order
19222 | Token::Limit
19223 | Token::Offset
19224 | Token::Semicolon
19225 | Token::RParen
19226 | Token::Union
19227 | Token::Except
19228 | Token::Eof => true,
19229 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19230 // with unreserved keywords, so they arrive as plain idents.
19231 Token::Ident(s) => {
19232 s.eq_ignore_ascii_case("fetch")
19233 || s.eq_ignore_ascii_case("window")
19234 || s.eq_ignore_ascii_case("intersect")
19235 }
19236 _ => false,
19237 }
19238 }
19239
19240 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19241 if matches!(self.peek(), Token::Star) {
19242 self.advance();
19243 return Ok(SelectItem::Wildcard);
19244 }
19245 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19246 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19247 // choke on the `*` ("expected identifier, got Star"). The lookahead is
19248 // `<ident> . *` with nothing binding tighter.
19249 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19250 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19251 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19252 {
19253 self.advance(); // qualifier
19254 self.advance(); // .
19255 self.advance(); // *
19256 return Ok(SelectItem::QualifiedWildcard(q));
19257 }
19258 }
19259 let start_tok = self.pos;
19260 let expr = self.parse_expr(0)?;
19261 let end_tok = self.consumed_pos();
19262 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19263 // multi-column function returns into columns. Marked here and lowered in
19264 // `parse_bare_select`, where the FROM clause is in hand.
19265 if matches!(self.peek(), Token::Dot)
19266 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19267 {
19268 self.advance(); // .
19269 self.advance(); // *
19270 return Ok(SelectItem::Expr {
19271 expr: Expr::FunctionCall {
19272 name: "__record_expand".to_string(),
19273 args: alloc::vec![expr],
19274 },
19275 alias: None,
19276 });
19277 }
19278 // v7.39.2 — MySQL lets a STRING name a projection item, with or
19279 // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19280 // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19281 // `syntax error at or near "'x'"` to all of them.
19282 //
19283 // Only here, not in `parse_optional_alias`: that one also names
19284 // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19285 // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19286 // after the lexer's own rule has joined adjacent literals, or
19287 // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19288 // MySQL answers the concatenation `ab`.
19289 if self.mysql_dialect {
19290 let at_as = matches!(self.peek(), Token::As)
19291 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19292 if at_as {
19293 self.advance();
19294 }
19295 if let Token::String(name) = self.peek().clone() {
19296 self.advance();
19297 return Ok(SelectItem::Expr {
19298 expr,
19299 alias: Some(name),
19300 });
19301 }
19302 }
19303 let alias = match self.parse_optional_alias()? {
19304 Some(a) => Some(a),
19305 None => self.mysql_item_label(&expr, start_tok, end_tok),
19306 };
19307 Ok(SelectItem::Expr { expr, alias })
19308 }
19309
19310 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19311 /// carries no `AS`, filled in here so every downstream path reports it
19312 /// without knowing the rule. `None` leaves the item un-aliased, which is
19313 /// what a PG session always gets.
19314 ///
19315 /// Measured against MariaDB 11, three rules and no more:
19316 ///
19317 /// | item | label | why |
19318 /// |------------------|------------|------------------------------|
19319 /// | `lbl.a` | `a` | a column reports its name |
19320 /// | `'it''s'` | `it's` | a string reports its VALUE |
19321 /// | `a + b` | `a + b` | anything else, source text |
19322 ///
19323 /// The third is why this lives in the parser at all: the label is the
19324 /// text the client WROTE, down to the spacing, so it cannot be printed
19325 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19326 ///
19327 /// Comments survive, and that is right: through a `mariadb` CLI both
19328 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
19329 /// CLIENT stripping the comment before it sends. Asked over the raw
19330 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19331 /// produces.
19332 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19333 if !self.mysql_dialect {
19334 return None;
19335 }
19336 match expr {
19337 // A column already reports its own name downstream; naming it
19338 // again here would only re-state the qualifier the label drops.
19339 Expr::Column(_) => None,
19340 // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19341 // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19342 // the first segment as written, not the joined value
19343 // (measured). The lexer logs where it joined them.
19344 Expr::Literal(Literal::String(v)) => Some(
19345 self.merged_first_len(start_tok)
19346 .and_then(|n| v.get(..n))
19347 .map_or_else(|| v.clone(), String::from),
19348 ),
19349 _ => self.source_span(start_tok, end_tok).map(str::to_string),
19350 }
19351 }
19352
19353 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19354 /// consumed VALUES keyword. Each row lowers to a constant SELECT
19355 /// with PG's default column1..columnN names; subsequent rows
19356 /// chain as UNION ALL peers. Shared by the FROM-position
19357 /// `( VALUES … )` arm and the top-level bare VALUES statement.
19358 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19359 let mut row_selects: Vec<SelectStatement> = Vec::new();
19360 loop {
19361 if !matches!(self.peek(), Token::LParen) {
19362 return Err(self.err(alloc::format!(
19363 "expected '(' to start a VALUES row, got {:?}",
19364 self.peek()
19365 )));
19366 }
19367 self.advance(); // (
19368 let mut items: Vec<SelectItem> = Vec::new();
19369 loop {
19370 let expr = self.parse_expr(0)?;
19371 items.push(SelectItem::Expr {
19372 expr,
19373 alias: Some(alloc::format!("column{}", items.len() + 1)),
19374 });
19375 match self.peek() {
19376 Token::Comma => {
19377 self.advance();
19378 }
19379 Token::RParen => break,
19380 other => {
19381 return Err(self.err(alloc::format!(
19382 "expected ',' or ')' in VALUES row, got {other:?}"
19383 )));
19384 }
19385 }
19386 }
19387 self.advance(); // )
19388 row_selects.push(SelectStatement {
19389 locking: None,
19390 ctes: Vec::new(),
19391 distinct: false,
19392 distinct_on: Vec::new(),
19393 items,
19394 from: None,
19395 where_: None,
19396 group_by: None,
19397 group_by_all: false,
19398 having: None,
19399 unions: Vec::new(),
19400 order_by: Vec::new(),
19401 limit: None,
19402 offset: None,
19403 limit_with_ties: false,
19404 window_check_exprs: Vec::new(),
19405 });
19406 if matches!(self.peek(), Token::Comma) {
19407 self.advance();
19408 continue;
19409 }
19410 break;
19411 }
19412 let mut head = row_selects.remove(0);
19413 head.unions = row_selects
19414 .into_iter()
19415 .map(|s| (UnionKind::All, s))
19416 .collect();
19417 Ok(head)
19418 }
19419
19420 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19421 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19422 // children. It was read as a table NAMED `only`, so the query
19423 // failed on `relation "only" does not exist`.
19424 //
19425 // v7.39 (round 644) — and it is no longer a no-op. Round 621
19426 // absorbed the keyword, reasoning that SPG's children are
19427 // separate relations a plain scan does not descend into, so ONLY
19428 // already described the scan. That stopped being true when a
19429 // partition parent started unioning its children: measured,
19430 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19431 // where PG answers 0. The flag is carried now.
19432 let mut only = false;
19433 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19434 && matches!(
19435 self.tokens.get(self.pos + 1),
19436 Some(Token::Ident(_) | Token::QuotedIdent(_))
19437 )
19438 {
19439 only = true;
19440 self.advance();
19441 }
19442 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19443 // for these SRFs the keyword is noise at parse time: the
19444 // join executor already substitutes outer-column references
19445 // into unnest_expr / generate_series_args per outer row
19446 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19447 // licences the correlation even without the keyword. Absorb
19448 // it and fall through to the SRF arms below.
19449 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19450 // just the four builtin SRFs: a user set-returning function on a JOIN's
19451 // right side is the whole point of LATERAL. The keyword stays noise at
19452 // parse time — the join executor substitutes the outer row into the
19453 // call's arguments per outer row.
19454 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19455 && matches!(
19456 self.tokens.get(self.pos + 1),
19457 // The json_each family has its OWN `LATERAL …` arm below, which
19458 // needs to see the keyword — absorbing it here would send those
19459 // calls down the generic table-function channel instead.
19460 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19461 )
19462 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19463 {
19464 self.advance(); // LATERAL
19465 }
19466 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19467 // set-returning function whose argument may reference a
19468 // preceding FROM item. We rewrite this to
19469 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19470 // AS __srf__) AS <alias>` so the existing LATERAL subquery
19471 // executor handles per-outer-row evaluation and the
19472 // SRF-primary jsonb_each_text path handles the inner
19473 // materialisation. Sentori 0067 backfill is the dogfood
19474 // shape.
19475 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19476 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19477 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19478 {
19479 self.advance(); // LATERAL
19480 let each_fn = match self.peek() {
19481 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19482 _ => unreachable!(),
19483 };
19484 self.advance(); // jsonb_each[_text] / json_each[_text]
19485 self.advance(); // (
19486 let arg = self.parse_expr(0)?;
19487 if !matches!(self.peek(), Token::RParen) {
19488 return Err(self.err(alloc::format!(
19489 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19490 self.peek()
19491 )));
19492 }
19493 self.advance();
19494 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19495 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19496 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19497 // FROM jsonb_each_text(<arg>) AS __srf__
19498 // PG's `AS kv(key, value)` column-alias list maps
19499 // positions to names; default to (key, value) when
19500 // omitted (matching the SRF's natural column names).
19501 let srf_alias = "__srf__".to_string();
19502 let key_alias = column_aliases
19503 .first()
19504 .cloned()
19505 .unwrap_or_else(|| "key".to_string());
19506 let value_alias = column_aliases
19507 .get(1)
19508 .cloned()
19509 .unwrap_or_else(|| "value".to_string());
19510 let inner_select = crate::ast::SelectStatement {
19511 locking: None,
19512 ctes: Vec::new(),
19513 distinct: false,
19514 distinct_on: Vec::new(),
19515 items: alloc::vec![
19516 crate::ast::SelectItem::Expr {
19517 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19518 qualifier: Some(srf_alias.clone()),
19519 name: "key".to_string(),
19520 }),
19521 alias: Some(key_alias),
19522 },
19523 crate::ast::SelectItem::Expr {
19524 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19525 qualifier: Some(srf_alias.clone()),
19526 name: "value".to_string(),
19527 }),
19528 alias: Some(value_alias),
19529 },
19530 ],
19531 from: Some(crate::ast::FromClause {
19532 primary: TableRef {
19533 name: srf_alias.clone(),
19534 alias: Some(srf_alias.clone()),
19535 only: false,
19536 as_of_segment: None,
19537 unnest_expr: None,
19538 unnest_column_aliases: Vec::new(),
19539 with_ordinality: false,
19540 generate_series_args: None,
19541 lateral_subquery: None,
19542 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19543 table_fn_call: None,
19544 rows_from: None,
19545 json_table: None,
19546 scalar_fn_item: false,
19547 },
19548 joins: Vec::new(),
19549 }),
19550 where_: None,
19551 group_by: None,
19552 group_by_all: false,
19553 having: None,
19554 unions: Vec::new(),
19555 order_by: Vec::new(),
19556 limit: None,
19557 offset: None,
19558 limit_with_ties: false,
19559 window_check_exprs: Vec::new(),
19560 };
19561 return Ok(TableRef {
19562 name: alias.clone(),
19563 alias: Some(alias),
19564 only: false,
19565 as_of_segment: None,
19566 unnest_expr: None,
19567 unnest_column_aliases: Vec::new(),
19568 with_ordinality: false,
19569 generate_series_args: None,
19570 lateral_subquery: Some(Box::new(inner_select)),
19571 jsonb_each_text_arg: None,
19572 table_fn_call: None,
19573 rows_from: None,
19574 json_table: None,
19575 scalar_fn_item: false,
19576 });
19577 }
19578 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19579 // without an explicit `LATERAL` keyword is the same shape
19580 // PG accepts (SRF naturally licences lateral correlation).
19581 // We mirror the LATERAL rewrite when the argument syntactic-
19582 // ally references an outer column (Column { qualifier:
19583 // Some(_), … }). For simplicity we apply the rewrite
19584 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19585 // in the FROM-list — caller-side join parsing positions
19586 // this peek correctly.
19587 // (Implementation note: detection lives below; the LATERAL
19588 // branch above already covers the explicit form; the bare
19589 // form falls through to the plain SRF arm and the engine
19590 // treats it as a constant-arg SRF if no outer reference is
19591 // present.)
19592 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19593 // table. Detect at the head so it claims precedence over
19594 // every other table-ref shape (unnest / generate_series /
19595 // bare ident); the lateral subquery itself follows the
19596 // regular SELECT grammar.
19597 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19598 // t(cols)`. Each row lowers to a constant SELECT with PG's
19599 // default column1..columnN names; subsequent rows chain as
19600 // UNION ALL peers. The result rides the derived-table
19601 // lateral_subquery channel — zero executor work.
19602 if matches!(self.peek(), Token::LParen)
19603 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19604 {
19605 self.advance(); // (
19606 self.advance(); // VALUES
19607 let head = self.parse_values_rows_body()?;
19608 if !matches!(self.peek(), Token::RParen) {
19609 return Err(self.err(alloc::format!(
19610 "expected ')' after VALUES list, got {:?}",
19611 self.peek()
19612 )));
19613 }
19614 self.advance();
19615 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19616 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19617 return Ok(TableRef {
19618 name,
19619 alias: alias_ident,
19620 only: false,
19621 as_of_segment: None,
19622 unnest_expr: None,
19623 unnest_column_aliases: column_aliases,
19624 with_ordinality: false,
19625 generate_series_args: None,
19626 lateral_subquery: Some(Box::new(head)),
19627 jsonb_each_text_arg: None,
19628 table_fn_call: None,
19629 rows_from: None,
19630 json_table: None,
19631 scalar_fn_item: false,
19632 });
19633 }
19634 // v7.37.17 (17.6 siblings) — plain derived table:
19635 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19636 // lateral_subquery channel the explicit LATERAL form uses —
19637 // an uncorrelated inner SELECT executes identically. The
19638 // inner parse carries UNION tails (they live on
19639 // SelectStatement.unions).
19640 // v7.37 D.20 — the derived-table inner may itself be a
19641 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19642 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19643 // bare `(SELECT …)`. parse_one_statement already routes a leading
19644 // `(` set-op group (its LParen arm) and a leading WITH
19645 // (parse_with_cte_then_select), so widen the second-token gate to
19646 // Select | LParen | WITH.
19647 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19648 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19649 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19650 // has existed since the shorthand landed and `parse_bare_select`
19651 // already routes it ("valid anywhere a SELECT head is"); what was
19652 // missing is this second-token gate, and the CTE body's dispatch
19653 // below. Round 868 found both by putting the shorthand in a
19654 // subquery — the top-level forms had been the only ones tested.
19655 if matches!(self.peek(), Token::LParen)
19656 && (matches!(
19657 self.tokens.get(self.pos + 1),
19658 Some(Token::Select | Token::LParen | Token::Table)
19659 ) || matches!(self.tokens.get(self.pos + 1),
19660 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19661 {
19662 self.advance(); // (
19663 let inner = match self.parse_one_statement()? {
19664 Statement::Select(s) => s,
19665 other => {
19666 return Err(self.err(alloc::format!(
19667 "expected SELECT inside derived table ( … ), got {other:?}"
19668 )));
19669 }
19670 };
19671 if !matches!(self.peek(), Token::RParen) {
19672 return Err(self.err(alloc::format!(
19673 "expected ')' after derived-table subquery, got {:?}",
19674 self.peek()
19675 )));
19676 }
19677 self.advance();
19678 // `AS t(a, b)` column-alias list rides the
19679 // unnest_column_aliases field (same positional-rename
19680 // contract the unnest SRFs use).
19681 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19682 let name = alias_ident
19683 .clone()
19684 .unwrap_or_else(|| "subquery".to_string());
19685 return Ok(TableRef {
19686 name,
19687 alias: alias_ident,
19688 only: false,
19689 as_of_segment: None,
19690 unnest_expr: None,
19691 unnest_column_aliases: column_aliases,
19692 with_ordinality: false,
19693 generate_series_args: None,
19694 lateral_subquery: Some(Box::new(inner)),
19695 jsonb_each_text_arg: None,
19696 table_fn_call: None,
19697 rows_from: None,
19698 json_table: None,
19699 scalar_fn_item: false,
19700 });
19701 }
19702 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19703 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19704 {
19705 self.advance(); // LATERAL
19706 self.advance(); // (
19707 // Parse the inner SELECT.
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 LATERAL ( … ), got {other:?}"
19713 )));
19714 }
19715 };
19716 if !matches!(self.peek(), Token::RParen) {
19717 return Err(self.err(alloc::format!(
19718 "expected ')' after LATERAL subquery, got {:?}",
19719 self.peek()
19720 )));
19721 }
19722 self.advance();
19723 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19724 // `(VALUES …) t(g)` derived table round-trips through view-body
19725 // Display, which renders on the lateral_subquery channel).
19726 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19727 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19728 return Ok(TableRef {
19729 name,
19730 alias: alias_ident,
19731 only: false,
19732 as_of_segment: None,
19733 unnest_expr: None,
19734 unnest_column_aliases: column_aliases,
19735 with_ordinality: false,
19736 generate_series_args: None,
19737 lateral_subquery: Some(Box::new(inner)),
19738 jsonb_each_text_arg: None,
19739 table_fn_call: None,
19740 rows_from: None,
19741 json_table: None,
19742 scalar_fn_item: false,
19743 });
19744 }
19745 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19746 // function as a FROM item. Emits one row per (key, value)
19747 // pair in the JSONB object argument as TEXT columns. May
19748 // be wrapped in CROSS JOIN LATERAL when the argument
19749 // references a preceding FROM item (sentori migration
19750 // 0067 backfill shape: `CROSS JOIN LATERAL
19751 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19752 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19753 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19754 {
19755 let each_fn = match self.peek() {
19756 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19757 _ => unreachable!(),
19758 };
19759 self.advance(); // jsonb_each[_text] / json_each[_text]
19760 self.advance(); // (
19761 let arg = self.parse_expr(0)?;
19762 if !matches!(self.peek(), Token::RParen) {
19763 return Err(self.err(alloc::format!(
19764 "expected ')' after {each_fn}() argument, got {:?}",
19765 self.peek()
19766 )));
19767 }
19768 self.advance();
19769 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19770 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19771 return Ok(TableRef {
19772 name,
19773 alias: alias_ident,
19774 only: false,
19775 as_of_segment: None,
19776 unnest_expr: None,
19777 // `AS t(k, v)` renames key/value positionally, same as the
19778 // LATERAL-position form already does.
19779 unnest_column_aliases: column_aliases,
19780 with_ordinality: false,
19781 generate_series_args: None,
19782 lateral_subquery: None,
19783 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19784 table_fn_call: None,
19785 rows_from: None,
19786 json_table: None,
19787 scalar_fn_item: false,
19788 });
19789 }
19790 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19791 // (+ json_ variants) — record-returning JSON functions with a
19792 // column-definition list. Desugar to a derived table that
19793 // projects each declared column from the JSON via `->>` + a cast,
19794 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19795 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19796 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19797 {
19798 return self.parse_json_to_record_from();
19799 }
19800 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19801 // row is a text[] of capture groups, so it cannot desugar to unnest
19802 // (that would flatten the array). Wrap it as a derived table
19803 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19804 // SRF path already emits one text[] row per match. PG names the column
19805 // `regexp_matches`; an `AS a(col)` alias overrides it.
19806 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19807 if s.eq_ignore_ascii_case("regexp_matches"))
19808 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19809 {
19810 self.advance(); // fn name
19811 self.advance(); // (
19812 let mut fn_args: Vec<Expr> = Vec::new();
19813 loop {
19814 fn_args.push(self.parse_expr(0)?);
19815 if matches!(self.peek(), Token::Comma) {
19816 self.advance();
19817 continue;
19818 }
19819 break;
19820 }
19821 if !matches!(self.peek(), Token::RParen) {
19822 return Err(self.err(alloc::format!(
19823 "expected ')' after regexp_matches() arguments, got {:?}",
19824 self.peek()
19825 )));
19826 }
19827 self.advance();
19828 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19829 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19830 // it, so it died on the `with` token while every other table function
19831 // accepted it.
19832 let with_ordinality = self.absorb_with_ordinality();
19833 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19834 let table_alias = alias_ident
19835 .clone()
19836 .unwrap_or_else(|| "regexp_matches".to_string());
19837 // PG names a single-column function's output column after the ALIAS
19838 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19839 // `m` reads as that column and not as a whole-row composite. Naming
19840 // it after the function regardless made `SELECT m[1] FROM … AS m`
19841 // subscript a record.
19842 let col_name = column_aliases
19843 .first()
19844 .cloned()
19845 .or_else(|| alias_ident.clone())
19846 .unwrap_or_else(|| "regexp_matches".to_string());
19847 let inner = crate::ast::SelectStatement {
19848 locking: None,
19849 ctes: Vec::new(),
19850 distinct: false,
19851 distinct_on: Vec::new(),
19852 items: alloc::vec![SelectItem::Expr {
19853 expr: Expr::FunctionCall {
19854 name: "regexp_matches".to_string(),
19855 args: fn_args,
19856 },
19857 alias: Some(col_name),
19858 }],
19859 from: None,
19860 where_: None,
19861 group_by: None,
19862 group_by_all: false,
19863 having: None,
19864 unions: Vec::new(),
19865 order_by: Vec::new(),
19866 limit: None,
19867 offset: None,
19868 limit_with_ties: false,
19869 window_check_exprs: Vec::new(),
19870 };
19871 return Ok(TableRef {
19872 name: table_alias.clone(),
19873 alias: Some(table_alias),
19874 only: false,
19875 as_of_segment: None,
19876 unnest_expr: None,
19877 unnest_column_aliases: column_aliases,
19878 with_ordinality,
19879 generate_series_args: None,
19880 lateral_subquery: Some(Box::new(inner)),
19881 jsonb_each_text_arg: None,
19882 table_fn_call: None,
19883 rows_from: None,
19884 json_table: None,
19885 // regexp_matches returns text[], a base type: `SELECT m FROM
19886 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19887 scalar_fn_item: true,
19888 });
19889 }
19890 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19891 // / json_ variants as a FROM item. Rewritten into
19892 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19893 // elements as a TEXT array, and the existing unnest SRF path
19894 // materialises one row per element. PG's natural column name
19895 // is `value`; an `AS a(col)` column-alias list overrides it.
19896 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19897 if s.eq_ignore_ascii_case("jsonb_array_elements")
19898 || s.eq_ignore_ascii_case("json_array_elements")
19899 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19900 || s.eq_ignore_ascii_case("json_array_elements_text")
19901 || s.eq_ignore_ascii_case("jsonb_object_keys")
19902 || s.eq_ignore_ascii_case("json_object_keys")
19903 || s.eq_ignore_ascii_case("jsonb_path_query")
19904 || s.eq_ignore_ascii_case("json_path_query")
19905 || s.eq_ignore_ascii_case("generate_subscripts")
19906 || s.eq_ignore_ascii_case("string_to_table")
19907 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19908 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19909 {
19910 let fn_name = match self.peek() {
19911 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19912 _ => unreachable!(),
19913 };
19914 self.advance(); // fn name
19915 self.advance(); // (
19916 let mut fn_args: Vec<Expr> = Vec::new();
19917 loop {
19918 fn_args.push(self.parse_expr(0)?);
19919 if matches!(self.peek(), Token::Comma) {
19920 self.advance();
19921 continue;
19922 }
19923 break;
19924 }
19925 if !matches!(self.peek(), Token::RParen) {
19926 return Err(self.err(alloc::format!(
19927 "expected ')' after {fn_name}() arguments, got {:?}",
19928 self.peek()
19929 )));
19930 }
19931 self.advance();
19932 let with_ordinality = self.absorb_with_ordinality();
19933 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19934 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19935 // PG's natural column name: the array-elements SRFs
19936 // declare an OUT parameter `value`; jsonb_object_keys
19937 // and generate_subscripts have none, so the column is
19938 // named after the function. A bare table alias on a
19939 // single-column SRF renames the column too (PG: `FROM
19940 // generate_subscripts(a, 1) AS s` projects column s) —
19941 // except for the OUT-parameter SRFs, whose column stays
19942 // `value` under a bare alias.
19943 let natural_col = if fn_name.ends_with("_array_elements")
19944 || fn_name.ends_with("_array_elements_text")
19945 {
19946 "value".to_string()
19947 } else {
19948 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19949 };
19950 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19951 // Keep any further entries — the second names the
19952 // ordinality column under WITH ORDINALITY.
19953 srf_cols.extend(column_aliases.into_iter().skip(1));
19954 // The *_to_table SRFs are row-streams over the existing
19955 // *_to_array scalars — map the call target; the display
19956 // name (alias / column defaults) keeps the SRF spelling.
19957 let call_name = match fn_name.as_str() {
19958 "string_to_table" => "string_to_array".to_string(),
19959 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19960 _ => fn_name,
19961 };
19962 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19963 // preceding FROM item (bare or qualified column) is correlated;
19964 // route it through the per-outer-row lateral channel.
19965 let expr = crate::ast::Expr::FunctionCall {
19966 name: call_name,
19967 args: fn_args,
19968 };
19969 let correlated = Self::expr_has_any_column(&expr);
19970 let tref = TableRef {
19971 name,
19972 alias: alias_ident,
19973 only: false,
19974 as_of_segment: None,
19975 unnest_expr: Some(Box::new(expr)),
19976 unnest_column_aliases: srf_cols,
19977 with_ordinality,
19978 generate_series_args: None,
19979 lateral_subquery: None,
19980 jsonb_each_text_arg: None,
19981 table_fn_call: None,
19982 rows_from: None,
19983 json_table: None,
19984 // Each of these returns a BASE type (jsonb / text / int), so the item's
19985 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19986 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19987 scalar_fn_item: !with_ordinality,
19988 };
19989 return Ok(if correlated {
19990 Self::wrap_correlated_srf(tref)
19991 } else {
19992 tref
19993 });
19994 }
19995 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19996 // explicit parallel-zip syntax. Each entry lowers to its
19997 // array-returning scalar form (unnest(x) → x itself; the
19998 // FROM-SRF rewrite family → their scalar array calls) and
19999 // the list rides the multi-arg unnest zip channel:
20000 // NULL-padded to the longest, WITH ORDINALITY appends the
20001 // counter. generate_series has no scalar array form and
20002 // errors honestly.
20003 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20004 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20005 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20006 {
20007 self.advance(); // ROWS
20008 self.advance(); // FROM
20009 self.advance(); // (
20010 let mut entries: Vec<Expr> = Vec::new();
20011 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20012 // Used only when some entry has no array form.
20013 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20014 loop {
20015 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20016 if !matches!(self.peek(), Token::LParen) {
20017 return Err(self.err(alloc::format!(
20018 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20019 self.peek()
20020 )));
20021 }
20022 self.advance();
20023 let mut fn_args: Vec<Expr> = Vec::new();
20024 if !matches!(self.peek(), Token::RParen) {
20025 loop {
20026 fn_args.push(self.parse_expr(0)?);
20027 if matches!(self.peek(), Token::Comma) {
20028 self.advance();
20029 continue;
20030 }
20031 break;
20032 }
20033 }
20034 if !matches!(self.peek(), Token::RParen) {
20035 return Err(self.err(alloc::format!(
20036 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20037 self.peek()
20038 )));
20039 }
20040 self.advance();
20041 let entry = match fn_name.as_str() {
20042 "unnest" => {
20043 if fn_args.len() != 1 {
20044 return Err(
20045 self.err("unnest inside ROWS FROM takes exactly one array".into())
20046 );
20047 }
20048 fn_args.pop().expect("len checked")
20049 }
20050 "jsonb_array_elements"
20051 | "json_array_elements"
20052 | "jsonb_array_elements_text"
20053 | "json_array_elements_text"
20054 | "jsonb_object_keys"
20055 | "json_object_keys"
20056 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20057 name: fn_name,
20058 args: fn_args,
20059 },
20060 "string_to_table" => crate::ast::Expr::FunctionCall {
20061 name: "string_to_array".to_string(),
20062 args: fn_args,
20063 },
20064 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20065 name: "regexp_split_to_array".to_string(),
20066 args: fn_args,
20067 },
20068 // v7.39 (read01 round 74) — an SRF with no array form
20069 // (`generate_series`, a user `RETURNS SETOF` function) has no
20070 // scalar expression to zip, so the WHOLE list switches to the
20071 // rows_from channel, which runs each function and zips the
20072 // rows themselves. The all-array case keeps the old lowering:
20073 // it is well-trodden and this must not disturb it.
20074 _ => {
20075 generic.push((fn_name, fn_args));
20076 if matches!(self.peek(), Token::Comma) {
20077 self.advance();
20078 continue;
20079 }
20080 break;
20081 }
20082 };
20083 generic.push((
20084 // The array-able entries carry their lowered expr along, so a
20085 // MIXED list still works: the engine sees the scalar array
20086 // form and unnests it.
20087 "__array".to_string(),
20088 alloc::vec![entry.clone()],
20089 ));
20090 entries.push(entry);
20091 if matches!(self.peek(), Token::Comma) {
20092 self.advance();
20093 continue;
20094 }
20095 break;
20096 }
20097 if !matches!(self.peek(), Token::RParen) {
20098 return Err(self.err(alloc::format!(
20099 "expected ')' to close ROWS FROM, got {:?}",
20100 self.peek()
20101 )));
20102 }
20103 self.advance();
20104 let with_ordinality = self.absorb_with_ordinality();
20105 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20106 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20107 // v7.39 (read01 round 74) — some entry had no array form, so the whole
20108 // list rides the generic channel.
20109 if generic.iter().any(|(n, _)| n != "__array") {
20110 let correlated = generic
20111 .iter()
20112 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20113 let tref = TableRef {
20114 name,
20115 alias: alias_ident,
20116 only: false,
20117 as_of_segment: None,
20118 unnest_expr: None,
20119 unnest_column_aliases,
20120 with_ordinality,
20121 generate_series_args: None,
20122 lateral_subquery: None,
20123 jsonb_each_text_arg: None,
20124 table_fn_call: None,
20125 rows_from: Some(generic),
20126 json_table: None,
20127 scalar_fn_item: false,
20128 };
20129 return Ok(if correlated {
20130 Self::wrap_correlated_srf(tref)
20131 } else {
20132 tref
20133 });
20134 }
20135 let correlated = entries.iter().any(Self::expr_has_any_column);
20136 let expr = if entries.len() == 1 {
20137 entries.pop().expect("len checked")
20138 } else {
20139 crate::ast::Expr::FunctionCall {
20140 name: "__unnest_zip".to_string(),
20141 args: entries,
20142 }
20143 };
20144 let tref = TableRef {
20145 name,
20146 alias: alias_ident,
20147 only: false,
20148 as_of_segment: None,
20149 unnest_expr: Some(Box::new(expr)),
20150 unnest_column_aliases,
20151 with_ordinality,
20152 generate_series_args: None,
20153 lateral_subquery: None,
20154 jsonb_each_text_arg: None,
20155 table_fn_call: None,
20156 rows_from: None,
20157 json_table: None,
20158 scalar_fn_item: false,
20159 };
20160 return Ok(if correlated {
20161 Self::wrap_correlated_srf(tref)
20162 } else {
20163 tref
20164 });
20165 }
20166 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20167 // source. Detect at the head before the bare-ident fallback;
20168 // unnest is not a reserved token.
20169 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20170 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20171 {
20172 self.advance(); // unnest
20173 self.advance(); // (
20174 let mut srf_args = alloc::vec![self.parse_expr(0)?];
20175 while matches!(self.peek(), Token::Comma) {
20176 self.advance();
20177 srf_args.push(self.parse_expr(0)?);
20178 }
20179 if !matches!(self.peek(), Token::RParen) {
20180 return Err(self.err(alloc::format!(
20181 "expected ')' after unnest() argument, got {:?}",
20182 self.peek()
20183 )));
20184 }
20185 self.advance();
20186 // Multi-arg unnest(a, b, …) zips the arrays in
20187 // parallel, NULL-padding to the longest (PG's ROWS
20188 // FROM shorthand). Lower onto the unnest channel as an
20189 // internal marker call the executors unpack.
20190 let expr = if srf_args.len() == 1 {
20191 srf_args.pop().expect("len checked")
20192 } else {
20193 crate::ast::Expr::FunctionCall {
20194 name: "__unnest_zip".to_string(),
20195 args: srf_args,
20196 }
20197 };
20198 let with_ordinality = self.absorb_with_ordinality();
20199 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20200 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20201 let correlated = Self::expr_has_any_column(&expr);
20202 let tref = TableRef {
20203 name,
20204 alias: alias_ident,
20205 only: false,
20206 as_of_segment: None,
20207 unnest_expr: Some(Box::new(expr)),
20208 unnest_column_aliases,
20209 with_ordinality,
20210 generate_series_args: None,
20211 lateral_subquery: None,
20212 jsonb_each_text_arg: None,
20213 table_fn_call: None,
20214 rows_from: None,
20215 json_table: None,
20216 scalar_fn_item: false,
20217 };
20218 return Ok(if correlated {
20219 Self::wrap_correlated_srf(tref)
20220 } else {
20221 tref
20222 });
20223 }
20224 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20225 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20226 // generic table-fn arg parser can't read), so it is intercepted
20227 // here BEFORE the generic dispatch. The doc expr may reference
20228 // outer columns (implicit LATERAL) — same correlated-wrap rule.
20229 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20230 if s.eq_ignore_ascii_case("json_table"))
20231 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20232 {
20233 let tref = self.parse_json_table_ref()?;
20234 let correlated = tref
20235 .json_table
20236 .as_deref()
20237 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20238 return Ok(if correlated {
20239 Self::wrap_correlated_srf(tref)
20240 } else {
20241 tref
20242 });
20243 }
20244 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20245 // functions dispatched by name (`pg_partition_tree('t')`,
20246 // `pg_partition_ancestors('t')`). Same head-detection shape as
20247 // unnest; the engine executor owns the row shape per function.
20248 // v7.39 (read01 round 65) — and a USER function in FROM position
20249 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20250 // (generate_series / unnest / the json_each family) keep it — their arms
20251 // sit further down, so they are excluded here by name rather than by
20252 // ordering. Anything else that is an ident followed by `(` is a table
20253 // function; the engine executor decides whether it is a builtin, a
20254 // set-returning user function, or an error.
20255 // 7.38.1 S5.1 — pg_dump spells its table functions
20256 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20257 // strip the pg_catalog prefix here so the same head-detection
20258 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20259 // meaning.
20260 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20261 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20262 && matches!(
20263 self.tokens.get(self.pos + 2),
20264 Some(Token::Ident(_) | Token::QuotedIdent(_))
20265 )
20266 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20267 {
20268 self.advance(); // pg_catalog
20269 self.advance(); // .
20270 }
20271 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20272 if !s.eq_ignore_ascii_case("generate_series")
20273 && !s.eq_ignore_ascii_case("unnest")
20274 && !is_json_each_name(s))
20275 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20276 {
20277 // Body out-of-line — this parse sits on the FROM/subquery
20278 // recursion chain (debug frame-cliff discipline).
20279 // v7.39 (read01 round 69) — a call whose arguments reference an outer
20280 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20281 // outer row, so it rides the lateral channel. Same rule the unnest
20282 // arm uses.
20283 let tref = self.parse_table_fn_ref()?;
20284 let correlated = tref
20285 .table_fn_call
20286 .as_deref()
20287 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20288 return Ok(if correlated {
20289 Self::wrap_correlated_srf(tref)
20290 } else {
20291 tref
20292 });
20293 }
20294 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20295 // [, step])` set-returning source. Same shape as unnest:
20296 // detect at the head, parse the comma-separated arg list,
20297 // dispatch downstream through the engine's set-returning
20298 // path. Supports integer triplets (mailrs's `WITH row_no AS
20299 // (SELECT * FROM generate_series(1, N))` pattern) and
20300 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20301 // date-range iteration pattern, which pre-3.10 had no
20302 // direct equivalent in SPG).
20303 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20304 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20305 {
20306 self.advance(); // generate_series
20307 self.advance(); // (
20308 let mut args: Vec<Expr> = Vec::new();
20309 loop {
20310 args.push(self.parse_expr(0)?);
20311 if matches!(self.peek(), Token::Comma) {
20312 self.advance();
20313 continue;
20314 }
20315 break;
20316 }
20317 if !matches!(self.peek(), Token::RParen) {
20318 return Err(self.err(alloc::format!(
20319 "expected ')' after generate_series() arguments, got {:?}",
20320 self.peek()
20321 )));
20322 }
20323 self.advance();
20324 if args.len() < 2 || args.len() > 3 {
20325 return Err(self.err(alloc::format!(
20326 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20327 args.len()
20328 )));
20329 }
20330 let with_ordinality = self.absorb_with_ordinality();
20331 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20332 let name = alias_ident
20333 .clone()
20334 .unwrap_or_else(|| "generate_series".to_string());
20335 let correlated = args.iter().any(Self::expr_has_any_column);
20336 let tref = TableRef {
20337 name,
20338 alias: alias_ident,
20339 only: false,
20340 as_of_segment: None,
20341 unnest_expr: None,
20342 unnest_column_aliases: column_aliases,
20343 with_ordinality,
20344 generate_series_args: Some(args),
20345 lateral_subquery: None,
20346 jsonb_each_text_arg: None,
20347 table_fn_call: None,
20348 rows_from: None,
20349 json_table: None,
20350 scalar_fn_item: false,
20351 };
20352 return Ok(if correlated {
20353 Self::wrap_correlated_srf(tref)
20354 } else {
20355 tref
20356 });
20357 }
20358 // v7.16.2 — preserve information_schema / pg_catalog
20359 // qualifiers (mailrs round-10 A.3). The generic
20360 // `expect_ident_like` strip silently drops the schema;
20361 // we want the engine to recognise these PG meta tables
20362 // and synthesise rows from the live catalog. Produce a
20363 // synthetic name (`__spg_info_columns` etc.) so the
20364 // engine's SELECT-side router can dispatch without
20365 // clashing with any user-defined `columns` table.
20366 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20367 (synth, Some(orig))
20368 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20369 (synth, Some(orig))
20370 } else {
20371 (self.expect_ident_like()?, None)
20372 };
20373 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20374 // time-travel clause. Parse BEFORE the alias so the
20375 // alias can still ride at the tail (`tbl AS OF SEGMENT
20376 // '5' alias`). `AS` is a reserved keyword token, while
20377 // `OF` and `SEGMENT` are bare idents.
20378 let as_of_segment = if matches!(self.peek(), Token::As)
20379 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20380 {
20381 self.advance(); // AS
20382 self.advance(); // OF
20383 let kw = match self.peek().clone() {
20384 Token::Ident(s) | Token::QuotedIdent(s) => s,
20385 other => {
20386 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20387 }
20388 };
20389 if !kw.eq_ignore_ascii_case("segment") {
20390 return Err(self.err(format!(
20391 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20392 )));
20393 }
20394 self.advance();
20395 // Segment id literal — accept either a string or
20396 // integer for operator ergonomics.
20397 let id = match self.advance() {
20398 Token::String(s) => s
20399 .parse::<u32>()
20400 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20401 Token::Integer(n) => u32::try_from(n)
20402 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20403 other => {
20404 return Err(self.err(format!(
20405 "expected segment id literal after AS OF SEGMENT, got {other:?}"
20406 )));
20407 }
20408 };
20409 Some(id)
20410 } else {
20411 None
20412 };
20413 // TABLESAMPLE is not a reserved token — keep the bare-ident
20414 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20415 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20416 {
20417 None
20418 } else {
20419 self.parse_optional_alias()?
20420 };
20421 // r1052 — a catalog name rewritten to its synthetic form keeps
20422 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20423 // still binds after `pg_cast` became `__spg_pg_cast`. PG
20424 // semantics: the visible name of `pg_catalog.pg_cast` IS
20425 // `pg_cast`. Without this, every table-name-qualified column
20426 // on a synthesised catalog answered "missing FROM-clause
20427 // entry" — which is the wall pg_dump hit on its first
20428 // pg_proc/pg_cast query.
20429 let alias = match (&alias, &meta_original) {
20430 (None, Some(orig)) if *orig != name => Some(orig.clone()),
20431 _ => alias,
20432 };
20433 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20434 // (PG grammar). BERNOULLI lowers to a per-row
20435 // `random() < p/100` conjunct on the enclosing SELECT's
20436 // WHERE — exact row-level Bernoulli semantics. SYSTEM
20437 // shares the lowering: SPG has no page structure to
20438 // sample, and the row-level form returns the same expected
20439 // fraction. REPEATABLE(seed) promises a deterministic
20440 // sample SPG cannot honour yet — honest error rather than
20441 // a silently ignored seed.
20442 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20443 self.advance();
20444 let method = self.expect_ident_like()?;
20445 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20446 return Err(self.err(alloc::format!(
20447 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20448 )));
20449 }
20450 if !matches!(self.peek(), Token::LParen) {
20451 return Err(self.err(alloc::format!(
20452 "expected '(' after TABLESAMPLE {}, got {:?}",
20453 method.to_ascii_uppercase(),
20454 self.peek()
20455 )));
20456 }
20457 self.advance();
20458 let percent = self.parse_expr(0)?;
20459 if !matches!(self.peek(), Token::RParen) {
20460 return Err(self.err(alloc::format!(
20461 "expected ')' after TABLESAMPLE percentage, got {:?}",
20462 self.peek()
20463 )));
20464 }
20465 self.advance();
20466 // REPEATABLE(seed) → a deterministic per-row draw seeded by
20467 // `seed`, so the sample is stable across repeats and rescans.
20468 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20469 let mut sample_seed: Option<Expr> = None;
20470 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20471 self.advance();
20472 if !matches!(self.peek(), Token::LParen) {
20473 return Err(self.err(alloc::format!(
20474 "expected '(' after REPEATABLE, got {:?}",
20475 self.peek()
20476 )));
20477 }
20478 self.advance();
20479 let seed = self.parse_expr(0)?;
20480 if !matches!(self.peek(), Token::RParen) {
20481 return Err(self.err(alloc::format!(
20482 "expected ')' after REPEATABLE seed, got {:?}",
20483 self.peek()
20484 )));
20485 }
20486 self.advance();
20487 sample_seed = Some(seed);
20488 }
20489 let draw = match sample_seed {
20490 Some(seed) => Expr::FunctionCall {
20491 name: "__tsm_fract".to_string(),
20492 args: alloc::vec![seed],
20493 },
20494 None => Expr::FunctionCall {
20495 name: "random".to_string(),
20496 args: Vec::new(),
20497 },
20498 };
20499 self.pending_sample_preds.push(Expr::Binary {
20500 lhs: Box::new(draw),
20501 op: crate::ast::BinOp::Lt,
20502 rhs: Box::new(Expr::Binary {
20503 lhs: Box::new(percent),
20504 op: crate::ast::BinOp::Div,
20505 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20506 }),
20507 });
20508 }
20509 Ok(TableRef {
20510 name,
20511 alias,
20512 only,
20513 as_of_segment,
20514 unnest_expr: None,
20515 unnest_column_aliases: Vec::new(),
20516 with_ordinality: false,
20517 generate_series_args: None,
20518 lateral_subquery: None,
20519 jsonb_each_text_arg: None,
20520 table_fn_call: None,
20521 rows_from: None,
20522 json_table: None,
20523 scalar_fn_item: false,
20524 })
20525 }
20526
20527 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20528 /// but also accepts `AS alias(col [, col, …])` — the
20529 /// PG-standard table-function column-list form. The column
20530 /// list is only honoured when paired with `UNNEST(...)` in
20531 /// the parent; other call sites currently discard it.
20532 /// True when the expression tree contains a qualified column
20533 /// reference (`t.col`) — the syntactic marker that an SRF
20534 /// argument correlates with a preceding FROM item.
20535 fn expr_has_qualified_column(e: &Expr) -> bool {
20536 match e {
20537 Expr::Column(c) => c.qualifier.is_some(),
20538 Expr::Binary { lhs, rhs, .. } => {
20539 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20540 }
20541 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20542 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20543 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20544 Expr::Case {
20545 operand,
20546 branches,
20547 else_branch,
20548 } => {
20549 operand
20550 .as_deref()
20551 .is_some_and(Self::expr_has_qualified_column)
20552 || branches.iter().any(|(w, t)| {
20553 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20554 })
20555 || else_branch
20556 .as_deref()
20557 .is_some_and(Self::expr_has_qualified_column)
20558 }
20559 _ => false,
20560 }
20561 }
20562
20563 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20564 /// counts a bare (unqualified) column. A set-returning function has no
20565 /// input columns of its own, so ANY column in its arguments is an outer
20566 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20567 fn expr_has_any_column(e: &Expr) -> bool {
20568 match e {
20569 Expr::Column(_) => true,
20570 Expr::Binary { lhs, rhs, .. } => {
20571 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20572 }
20573 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20574 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20575 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20576 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20577 // constructor or subscript fell to the `_ => false` arm, so
20578 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20579 // channel and the eager peer eval answered `column "x" does
20580 // not exist` (the substitution walker already recurses both
20581 // shapes; only this detector was blind to them).
20582 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20583 Expr::ArraySubscript { target, index } => {
20584 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20585 }
20586 Expr::Case {
20587 operand,
20588 branches,
20589 else_branch,
20590 } => {
20591 operand.as_deref().is_some_and(Self::expr_has_any_column)
20592 || branches
20593 .iter()
20594 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20595 || else_branch
20596 .as_deref()
20597 .is_some_and(Self::expr_has_any_column)
20598 }
20599 _ => false,
20600 }
20601 }
20602
20603 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20604 /// `generate_series(1, t.n)`) into the lateral_subquery
20605 /// channel: `SELECT * FROM <srf>` executes per outer row with
20606 /// outer references substituted (v7.37.43-T4.5 machinery).
20607 /// Uncorrelated SRFs stay on their plain channels.
20608 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20609 let name = srf.name.clone();
20610 let alias = srf.alias.clone();
20611 let inner = crate::ast::SelectStatement {
20612 locking: None,
20613 ctes: Vec::new(),
20614 distinct: false,
20615 distinct_on: Vec::new(),
20616 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20617 from: Some(crate::ast::FromClause {
20618 primary: srf,
20619 joins: Vec::new(),
20620 }),
20621 where_: None,
20622 group_by: None,
20623 group_by_all: false,
20624 having: None,
20625 unions: Vec::new(),
20626 order_by: Vec::new(),
20627 limit: None,
20628 offset: None,
20629 limit_with_ties: false,
20630 window_check_exprs: Vec::new(),
20631 };
20632 TableRef {
20633 name,
20634 alias,
20635 only: false,
20636 as_of_segment: None,
20637 unnest_expr: None,
20638 unnest_column_aliases: Vec::new(),
20639 with_ordinality: false,
20640 generate_series_args: None,
20641 lateral_subquery: Some(Box::new(inner)),
20642 jsonb_each_text_arg: None,
20643 table_fn_call: None,
20644 rows_from: None,
20645 json_table: None,
20646 scalar_fn_item: false,
20647 }
20648 }
20649
20650 /// True when the expression tree contains an unresolved
20651 /// `OVER w` marker (see parse_over_clause).
20652 fn expr_has_named_window(e: &Expr) -> bool {
20653 match e {
20654 Expr::WindowFunction { partition_by, .. } => matches!(
20655 partition_by.as_slice(),
20656 [Expr::Column(c)] if matches!(
20657 c.qualifier.as_deref(),
20658 Some("__named_window__") | Some("__named_window_ref__")
20659 )
20660 ),
20661 Expr::Binary { lhs, rhs, .. } => {
20662 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20663 }
20664 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20665 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20666 Expr::Case {
20667 operand,
20668 branches,
20669 else_branch,
20670 } => {
20671 operand.as_deref().is_some_and(Self::expr_has_named_window)
20672 || branches.iter().any(|(w, t)| {
20673 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20674 })
20675 || else_branch
20676 .as_deref()
20677 .is_some_and(Self::expr_has_named_window)
20678 }
20679 _ => false,
20680 }
20681 }
20682
20683 /// v7.39 (round 705) — the NAMES the expression references through the
20684 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20685 /// definitions nothing referenced. Traversal mirrors
20686 /// `expr_has_named_window` above.
20687 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20688 match e {
20689 Expr::WindowFunction { partition_by, .. } => {
20690 if let [Expr::Column(c)] = partition_by.as_slice()
20691 && matches!(
20692 c.qualifier.as_deref(),
20693 Some("__named_window__") | Some("__named_window_ref__")
20694 )
20695 {
20696 into.push(c.name.clone());
20697 }
20698 }
20699 Expr::Binary { lhs, rhs, .. } => {
20700 Self::collect_named_window_refs(lhs, into);
20701 Self::collect_named_window_refs(rhs, into);
20702 }
20703 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20704 Self::collect_named_window_refs(expr, into);
20705 }
20706 Expr::FunctionCall { args, .. } => {
20707 for a in args {
20708 Self::collect_named_window_refs(a, into);
20709 }
20710 }
20711 Expr::Case {
20712 operand,
20713 branches,
20714 else_branch,
20715 } => {
20716 if let Some(o) = operand.as_deref() {
20717 Self::collect_named_window_refs(o, into);
20718 }
20719 for (w, t) in branches {
20720 Self::collect_named_window_refs(w, into);
20721 Self::collect_named_window_refs(t, into);
20722 }
20723 if let Some(eb) = else_branch.as_deref() {
20724 Self::collect_named_window_refs(eb, into);
20725 }
20726 }
20727 _ => {}
20728 }
20729 }
20730
20731 /// Inline named-window definitions into the `OVER w` markers.
20732 /// An unknown name errors (PG: window "w" does not exist).
20733 #[allow(clippy::type_complexity)]
20734 fn substitute_named_windows(
20735 e: &mut Expr,
20736 defs: &[(
20737 String,
20738 (
20739 Vec<Expr>,
20740 Vec<(Expr, bool, Option<bool>)>,
20741 Option<WindowFrame>,
20742 ),
20743 )],
20744 ) -> Result<(), String> {
20745 match e {
20746 Expr::WindowFunction {
20747 partition_by,
20748 order_by,
20749 frame,
20750 ..
20751 } => {
20752 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20753 // from the bare `OVER w1` (a plain reference).
20754 let named = match partition_by.as_slice() {
20755 [Expr::Column(c)] => match c.qualifier.as_deref() {
20756 Some("__named_window__") => Some((c.name.clone(), false)),
20757 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20758 _ => None,
20759 },
20760 _ => None,
20761 };
20762 if let Some((wname, is_copy)) = named {
20763 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20764 else {
20765 return Err(alloc::format!("window {wname:?} does not exist"));
20766 };
20767 if !is_copy {
20768 *partition_by = def.0.clone();
20769 *order_by = def.1.clone();
20770 *frame = def.2.clone();
20771 return Ok(());
20772 }
20773 // v7.39 (round 229) — PG's copy rules, probed against
20774 // 18.4: a copy inherits the partitioning, may supply an
20775 // ordering only when the base has none, and may not copy
20776 // a base that already carries a frame (its own frame
20777 // would be ambiguous with the inherited one).
20778 if !def.1.is_empty() && !order_by.is_empty() {
20779 return Err(alloc::format!(
20780 "cannot override ORDER BY clause of window \"{wname}\""
20781 ));
20782 }
20783 if def.2.is_some() {
20784 return Err(alloc::format!(
20785 "cannot copy window \"{wname}\" because it has a frame clause"
20786 ));
20787 }
20788 *partition_by = def.0.clone();
20789 if order_by.is_empty() {
20790 *order_by = def.1.clone();
20791 }
20792 }
20793 Ok(())
20794 }
20795 Expr::Binary { lhs, rhs, .. } => {
20796 Self::substitute_named_windows(lhs, defs)?;
20797 Self::substitute_named_windows(rhs, defs)
20798 }
20799 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20800 Self::substitute_named_windows(expr, defs)
20801 }
20802 Expr::FunctionCall { args, .. } => {
20803 for a in args {
20804 Self::substitute_named_windows(a, defs)?;
20805 }
20806 Ok(())
20807 }
20808 Expr::Case {
20809 operand,
20810 branches,
20811 else_branch,
20812 } => {
20813 if let Some(op) = operand {
20814 Self::substitute_named_windows(op, defs)?;
20815 }
20816 for (w, t) in branches {
20817 Self::substitute_named_windows(w, defs)?;
20818 Self::substitute_named_windows(t, defs)?;
20819 }
20820 if let Some(el) = else_branch {
20821 Self::substitute_named_windows(el, defs)?;
20822 }
20823 Ok(())
20824 }
20825 _ => Ok(()),
20826 }
20827 }
20828
20829 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20830 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20831 /// composition.
20832 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20833 debug_assert!(matches!(self.peek(), Token::Table));
20834 self.advance(); // TABLE
20835 let tname = self.expect_ident_like()?;
20836 Ok(SelectStatement {
20837 locking: None,
20838 ctes: Vec::new(),
20839 distinct: false,
20840 distinct_on: Vec::new(),
20841 items: alloc::vec![SelectItem::Wildcard],
20842 from: Some(FromClause {
20843 primary: TableRef {
20844 name: tname,
20845 alias: None,
20846 only: false,
20847 as_of_segment: None,
20848 unnest_expr: None,
20849 unnest_column_aliases: Vec::new(),
20850 with_ordinality: false,
20851 generate_series_args: None,
20852 lateral_subquery: None,
20853 jsonb_each_text_arg: None,
20854 table_fn_call: None,
20855 rows_from: None,
20856 json_table: None,
20857 scalar_fn_item: false,
20858 },
20859 joins: Vec::new(),
20860 }),
20861 where_: None,
20862 group_by: None,
20863 group_by_all: false,
20864 having: None,
20865 unions: Vec::new(),
20866 order_by: Vec::new(),
20867 limit: None,
20868 offset: None,
20869 limit_with_ties: false,
20870 window_check_exprs: Vec::new(),
20871 })
20872 }
20873
20874 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20875 /// variants) → a derived table that reads each declared column out of
20876 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20877 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20878 /// the scalar *record form projects a single row straight off `J`.
20879 /// Rides the existing lateral-subquery channel, so no new executor or
20880 /// AST is needed.
20881 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20882 use crate::ast::{
20883 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20884 };
20885 let fn_name = match self.peek() {
20886 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20887 _ => unreachable!("caller guarded is_json_to_record_name"),
20888 };
20889 self.advance(); // fn name
20890 self.advance(); // (
20891 let mut arg = self.parse_expr(0)?;
20892 // populate_record(base, json): the base only carries the record
20893 // type here — the JSON argument is the second expression.
20894 let mut base: Option<Expr> = None;
20895 if matches!(self.peek(), Token::Comma) {
20896 self.advance();
20897 base = Some(arg);
20898 arg = self.parse_expr(0)?;
20899 }
20900 if !matches!(self.peek(), Token::RParen) {
20901 return Err(self.err(alloc::format!(
20902 "expected ')' after {fn_name}() argument, got {:?}",
20903 self.peek()
20904 )));
20905 }
20906 self.advance(); // )
20907 let is_set = fn_name.ends_with("recordset");
20908 // `[AS] alias ( col type [, …] )` column-definition list.
20909 if matches!(self.peek(), Token::As) {
20910 self.advance();
20911 }
20912 let alias_opt = match self.peek() {
20913 Token::Ident(s) | Token::QuotedIdent(s) => {
20914 let a = s.clone();
20915 self.advance();
20916 Some(a)
20917 }
20918 _ => None,
20919 };
20920 // v7.39 (read01 round 76) — the populate family's canonical PG
20921 // spelling carries no column list at all: the row shape comes from
20922 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20923 // j)`). The parser has no catalog, so hand the two arguments to the
20924 // engine's table-function channel, which does. Only `*_to_record*`
20925 // (whose base is bare `record`) genuinely requires the list.
20926 if !matches!(self.peek(), Token::LParen) {
20927 if let Some(base_expr) = base {
20928 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20929 return Ok(TableRef {
20930 name: alias.clone(),
20931 alias: Some(alias),
20932 only: false,
20933 as_of_segment: None,
20934 unnest_expr: None,
20935 unnest_column_aliases: Vec::new(),
20936 with_ordinality: false,
20937 generate_series_args: None,
20938 lateral_subquery: None,
20939 jsonb_each_text_arg: None,
20940 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20941 rows_from: None,
20942 json_table: None,
20943 scalar_fn_item: false,
20944 });
20945 }
20946 return Err(self.err(alloc::format!(
20947 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20948 self.peek()
20949 )));
20950 }
20951 let Some(alias) = alias_opt else {
20952 return Err(self.err(alloc::format!(
20953 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20954 )));
20955 };
20956 self.advance(); // (
20957 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20958 loop {
20959 let col = self.expect_ident_like()?;
20960 let ty = self.parse_cast_target()?;
20961 coldefs.push((col, ty));
20962 if matches!(self.peek(), Token::Comma) {
20963 self.advance();
20964 continue;
20965 }
20966 if matches!(self.peek(), Token::RParen) {
20967 self.advance();
20968 break;
20969 }
20970 return Err(self.err(alloc::format!(
20971 "expected ',' or ')' in {fn_name} column list, got {:?}",
20972 self.peek()
20973 )));
20974 }
20975 if coldefs.is_empty() {
20976 return Err(self.err(alloc::format!(
20977 "{fn_name} column-definition list must declare at least one column"
20978 )));
20979 }
20980 // Per column: (base ->> 'col')::type AS col. The base is the
20981 // per-element `value` column for the *set form, or the argument
20982 // itself for the scalar record form.
20983 let items: Vec<SelectItem> = coldefs
20984 .into_iter()
20985 .map(|(col, ty)| {
20986 let base = if is_set {
20987 Expr::Column(ColumnName {
20988 qualifier: None,
20989 name: "value".to_string(),
20990 })
20991 } else {
20992 arg.clone()
20993 };
20994 SelectItem::Expr {
20995 expr: Expr::Cast {
20996 expr: Box::new(Expr::Binary {
20997 lhs: Box::new(base),
20998 op: BinOp::JsonGetText,
20999 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21000 }),
21001 target: ty,
21002 },
21003 alias: Some(col),
21004 }
21005 })
21006 .collect();
21007 let from = if is_set {
21008 let elem_fn = if fn_name.starts_with("jsonb") {
21009 "jsonb_array_elements"
21010 } else {
21011 "json_array_elements"
21012 };
21013 Some(FromClause {
21014 primary: TableRef {
21015 name: "value".to_string(),
21016 alias: None,
21017 only: false,
21018 as_of_segment: None,
21019 unnest_expr: Some(Box::new(Expr::FunctionCall {
21020 name: elem_fn.to_string(),
21021 args: alloc::vec![arg],
21022 })),
21023 unnest_column_aliases: alloc::vec!["value".to_string()],
21024 with_ordinality: false,
21025 generate_series_args: None,
21026 lateral_subquery: None,
21027 jsonb_each_text_arg: None,
21028 table_fn_call: None,
21029 rows_from: None,
21030 json_table: None,
21031 scalar_fn_item: false,
21032 },
21033 joins: Vec::new(),
21034 })
21035 } else {
21036 None
21037 };
21038 let inner = SelectStatement {
21039 locking: None,
21040 ctes: Vec::new(),
21041 distinct: false,
21042 distinct_on: Vec::new(),
21043 items,
21044 from,
21045 where_: None,
21046 group_by: None,
21047 group_by_all: false,
21048 having: None,
21049 unions: Vec::new(),
21050 order_by: Vec::new(),
21051 limit: None,
21052 offset: None,
21053 limit_with_ties: false,
21054 window_check_exprs: Vec::new(),
21055 };
21056 Ok(TableRef {
21057 name: alias.clone(),
21058 alias: Some(alias),
21059 only: false,
21060 as_of_segment: None,
21061 unnest_expr: None,
21062 unnest_column_aliases: Vec::new(),
21063 with_ordinality: false,
21064 generate_series_args: None,
21065 lateral_subquery: Some(Box::new(inner)),
21066 jsonb_each_text_arg: None,
21067 table_fn_call: None,
21068 rows_from: None,
21069 json_table: None,
21070 scalar_fn_item: false,
21071 })
21072 }
21073
21074 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21075 /// Returns true when the clause was present. `WITH` alone (a
21076 /// CTE can never start here) is not enough — the ORDINALITY
21077 /// ident must follow, so a stray WITH still errors downstream.
21078 fn absorb_with_ordinality(&mut self) -> bool {
21079 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21080 && matches!(self.tokens.get(self.pos + 1),
21081 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21082 {
21083 self.advance();
21084 self.advance();
21085 true
21086 } else {
21087 false
21088 }
21089 }
21090
21091 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21092 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21093 /// Out-of-line: the caller sits on the FROM recursion chain.
21094 #[inline(never)]
21095 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21096 let fn_name = match self.advance() {
21097 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21098 _ => unreachable!("caller peeked an ident"),
21099 };
21100 self.advance(); // (
21101 let mut args: Vec<Expr> = Vec::new();
21102 if !matches!(self.peek(), Token::RParen) {
21103 loop {
21104 args.push(self.parse_expr(0)?);
21105 if matches!(self.peek(), Token::Comma) {
21106 self.advance();
21107 continue;
21108 }
21109 break;
21110 }
21111 }
21112 if !matches!(self.peek(), Token::RParen) {
21113 return Err(self.err(alloc::format!(
21114 "expected ')' after {fn_name}() arguments, got {:?}",
21115 self.peek()
21116 )));
21117 }
21118 self.advance();
21119 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21120 // counter column rides after the function's own, and the alias list
21121 // names it.
21122 let with_ordinality = self.absorb_with_ordinality();
21123 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21124 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21125 Ok(TableRef {
21126 name,
21127 alias: alias_ident,
21128 only: false,
21129 as_of_segment: None,
21130 unnest_expr: None,
21131 unnest_column_aliases,
21132 with_ordinality,
21133 generate_series_args: None,
21134 lateral_subquery: None,
21135 jsonb_each_text_arg: None,
21136 table_fn_call: Some(Box::new((fn_name, args))),
21137 rows_from: None,
21138 json_table: None,
21139 scalar_fn_item: false,
21140 })
21141 }
21142
21143 /// v7.39 (round 205, JSON_TABLE) — parse
21144 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21145 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21146 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21147 #[inline(never)]
21148 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21149 self.advance(); // json_table
21150 self.advance(); // (
21151 let doc = Box::new(self.parse_expr(0)?);
21152 self.expect_comma_json_table()?;
21153 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21154 // Optional `PASSING <expr> AS <name> [, …]`.
21155 let mut passing: Vec<(String, Expr)> = Vec::new();
21156 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21157 self.advance();
21158 loop {
21159 let e = self.parse_expr(0)?;
21160 if !matches!(self.peek(), Token::As) {
21161 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21162 }
21163 self.advance();
21164 let vname = match self.advance() {
21165 Token::Ident(s) | Token::QuotedIdent(s) => s,
21166 other => {
21167 return Err(self.err(alloc::format!(
21168 "expected PASSING variable name, got {other:?}"
21169 )));
21170 }
21171 };
21172 passing.push((vname, e));
21173 if matches!(self.peek(), Token::Comma) {
21174 self.advance();
21175 continue;
21176 }
21177 break;
21178 }
21179 }
21180 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21181 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21182 }
21183 self.advance();
21184 let columns = self.parse_json_table_columns()?;
21185 if !matches!(self.peek(), Token::RParen) {
21186 return Err(self.err(alloc::format!(
21187 "expected ')' to close JSON_TABLE, got {:?}",
21188 self.peek()
21189 )));
21190 }
21191 self.advance();
21192 let alias_ident = self.parse_optional_alias()?;
21193 let name = alias_ident
21194 .clone()
21195 .unwrap_or_else(|| String::from("json_table"));
21196 Ok(TableRef {
21197 name,
21198 alias: alias_ident,
21199 only: false,
21200 as_of_segment: None,
21201 unnest_expr: None,
21202 unnest_column_aliases: Vec::new(),
21203 with_ordinality: false,
21204 generate_series_args: None,
21205 lateral_subquery: None,
21206 jsonb_each_text_arg: None,
21207 table_fn_call: None,
21208 rows_from: None,
21209 json_table: Some(Box::new(crate::ast::JsonTable {
21210 doc,
21211 row_path,
21212 columns,
21213 passing,
21214 })),
21215 scalar_fn_item: false,
21216 })
21217 }
21218
21219 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21220 if !matches!(self.peek(), Token::Comma) {
21221 return Err(self.err(alloc::format!(
21222 "expected ',' after JSON_TABLE document, got {:?}",
21223 self.peek()
21224 )));
21225 }
21226 self.advance();
21227 Ok(())
21228 }
21229
21230 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21231 match self.advance() {
21232 Token::String(s) => Ok(s),
21233 other => Err(self.err(alloc::format!(
21234 "expected {what} string literal, got {other:?}"
21235 ))),
21236 }
21237 }
21238
21239 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21240 #[inline(never)]
21241 fn parse_json_table_columns(
21242 &mut self,
21243 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21244 if !matches!(self.peek(), Token::LParen) {
21245 return Err(self.err("expected '(' after COLUMNS".into()));
21246 }
21247 self.advance();
21248 let mut cols = Vec::new();
21249 loop {
21250 cols.push(self.parse_json_table_one_column()?);
21251 if matches!(self.peek(), Token::Comma) {
21252 self.advance();
21253 continue;
21254 }
21255 break;
21256 }
21257 if !matches!(self.peek(), Token::RParen) {
21258 return Err(self.err(alloc::format!(
21259 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21260 self.peek()
21261 )));
21262 }
21263 self.advance();
21264 Ok(cols)
21265 }
21266
21267 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21268 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21269 // NESTED [PATH] '<p>' COLUMNS (...)
21270 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21271 self.advance();
21272 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21273 self.advance();
21274 }
21275 let path = self.parse_json_string_literal("NESTED PATH")?;
21276 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21277 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21278 }
21279 self.advance();
21280 let columns = self.parse_json_table_columns()?;
21281 return Ok(JsonTableColumn::Nested { path, columns });
21282 }
21283 // <name> ...
21284 let name = match self.advance() {
21285 Token::Ident(s) | Token::QuotedIdent(s) => s,
21286 other => {
21287 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21288 }
21289 };
21290 // <name> FOR ORDINALITY
21291 if matches!(self.peek(), Token::For) {
21292 self.advance();
21293 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21294 return Err(self.err("expected ORDINALITY after FOR".into()));
21295 }
21296 self.advance();
21297 return Ok(JsonTableColumn::Ordinality { name });
21298 }
21299 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21300 let ty = self.parse_column_type_name()?;
21301 let mut format_json = false;
21302 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21303 self.advance();
21304 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21305 return Err(self.err("expected JSON after FORMAT".into()));
21306 }
21307 self.advance();
21308 format_json = true;
21309 }
21310 let mut exists = false;
21311 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21312 self.advance();
21313 exists = true;
21314 }
21315 let mut path = alloc::format!("$.{name}");
21316 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21317 self.advance();
21318 path = self.parse_json_string_literal("column PATH")?;
21319 }
21320 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21321 // `FORMAT JSON` after PATH (alternate placement).
21322 self.advance();
21323 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21324 self.advance();
21325 }
21326 format_json = true;
21327 }
21328 let mut wrapper = false;
21329 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21330 self.advance();
21331 // optional CONDITIONAL/UNCONDITIONAL
21332 if matches!(self.peek(), Token::Ident(s)
21333 if s.eq_ignore_ascii_case("unconditional")
21334 || s.eq_ignore_ascii_case("conditional"))
21335 {
21336 self.advance();
21337 }
21338 if !matches!(self.peek(), Token::Ident(s)
21339 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21340 {
21341 return Err(self.err("expected WRAPPER after WITH".into()));
21342 }
21343 self.advance();
21344 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21345 if matches!(self.peek(), Token::Ident(s)
21346 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21347 {
21348 self.advance();
21349 }
21350 wrapper = true;
21351 }
21352 // ON EMPTY / ON ERROR clauses (two, in any order).
21353 let mut on_empty = JsonTableOnBehavior::Null;
21354 let mut on_error = JsonTableOnBehavior::Null;
21355 for _ in 0..2 {
21356 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21357 {
21358 self.advance();
21359 Some(JsonTableOnBehavior::Error)
21360 } else if matches!(self.peek(), Token::Null) {
21361 self.advance();
21362 Some(JsonTableOnBehavior::Null)
21363 } else if matches!(self.peek(), Token::Default) {
21364 self.advance();
21365 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21366 } else {
21367 None
21368 };
21369 let Some(behavior) = behavior else { break };
21370 // `ON {EMPTY|ERROR}`
21371 if !matches!(self.peek(), Token::On) {
21372 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21373 }
21374 self.advance();
21375 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21376 self.advance();
21377 on_empty = behavior;
21378 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21379 self.advance();
21380 on_error = behavior;
21381 } else {
21382 return Err(self.err("expected EMPTY or ERROR after ON".into()));
21383 }
21384 }
21385 Ok(JsonTableColumn::Regular {
21386 name,
21387 ty,
21388 path,
21389 exists,
21390 format_json,
21391 wrapper,
21392 on_empty,
21393 on_error,
21394 })
21395 }
21396
21397 fn parse_optional_alias_with_columns(
21398 &mut self,
21399 ) -> Result<(Option<String>, Vec<String>), ParseError> {
21400 let alias = self.parse_optional_alias()?;
21401 if alias.is_none() {
21402 return Ok((None, Vec::new()));
21403 }
21404 let mut cols: Vec<String> = Vec::new();
21405 if matches!(self.peek(), Token::LParen) {
21406 self.advance();
21407 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21408 self.advance();
21409 cols.push(s);
21410 if matches!(self.peek(), Token::Comma) {
21411 self.advance();
21412 continue;
21413 }
21414 break;
21415 }
21416 if matches!(self.peek(), Token::RParen) {
21417 self.advance();
21418 }
21419 }
21420 Ok((alias, cols))
21421 }
21422
21423 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21424 /// whose keyword token was already consumed and whose `(` is the
21425 /// current token. Factored out of `parse_atom` (and marked
21426 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21427 /// recursive `parse_atom` frame — inlining them there enlarges the
21428 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21429 /// against, risking an overflow before the budget triggers.
21430 #[inline(never)]
21431 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21432 self.advance(); // (
21433 let mut args = Vec::new();
21434 if !matches!(self.peek(), Token::RParen) {
21435 loop {
21436 args.push(self.parse_expr(0)?);
21437 match self.peek() {
21438 Token::Comma => {
21439 self.advance();
21440 }
21441 Token::RParen => break,
21442 other => {
21443 return Err(self.err(alloc::format!(
21444 "expected ',' or ')' in {name}() args, got {other:?}"
21445 )));
21446 }
21447 }
21448 }
21449 }
21450 self.advance(); // )
21451 Ok(Expr::FunctionCall {
21452 name: name.into(),
21453 args,
21454 })
21455 }
21456
21457 /// FROM-clause: a primary table reference plus zero-or-more joined
21458 /// peers expressed via either `, <table>` (cross-product, no ON) or
21459 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21460 /// v1.10 keeps the join list flat (left-associative nested-loop
21461 /// semantics).
21462 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21463 let primary = self.parse_table_ref()?;
21464 let primary_qual = primary
21465 .alias
21466 .clone()
21467 .unwrap_or_else(|| primary.name.clone());
21468 let joins = self.parse_from_joins(&primary_qual)?;
21469 Ok(FromClause { primary, joins })
21470 }
21471
21472 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21473 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21474 /// SAME grammar after its target table has already been consumed.
21475 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21476 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21477 /// be parsed forward, once.)
21478 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21479 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21480 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21481 /// desugaring, which needs a name for the left side of each equality.
21482 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21483 let mut joins = Vec::new();
21484 loop {
21485 // `, <table>` — cross-product with no ON.
21486 if matches!(self.peek(), Token::Comma) {
21487 self.advance();
21488 let table = self.parse_table_ref()?;
21489 joins.push(FromJoin {
21490 kind: JoinKind::Cross,
21491 table,
21492 on: None,
21493 using_cols: None,
21494 natural: false,
21495 });
21496 continue;
21497 }
21498 // v7.37.16 — optional leading `NATURAL` before the join
21499 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21500 // not a lexer keyword (it arrives as a bare Ident), so match
21501 // it case-insensitively here. When present, no ON/USING
21502 // clause is allowed — the common columns are resolved at
21503 // execution time.
21504 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21505 if natural {
21506 self.advance();
21507 }
21508 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21509 // CROSS JOIN, and bare JOIN (defaults to INNER).
21510 let kind =
21511 match self.peek() {
21512 Token::Inner => {
21513 self.advance();
21514 if !matches!(self.peek(), Token::Join) {
21515 return Err(self
21516 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21517 }
21518 self.advance();
21519 JoinKind::Inner
21520 }
21521 Token::Left => {
21522 self.advance();
21523 if matches!(self.peek(), Token::Outer) {
21524 self.advance();
21525 }
21526 if !matches!(self.peek(), Token::Join) {
21527 return Err(self.err(format!(
21528 "expected JOIN after LEFT [OUTER], got {:?}",
21529 self.peek()
21530 )));
21531 }
21532 self.advance();
21533 JoinKind::Left
21534 }
21535 Token::Cross => {
21536 self.advance();
21537 if !matches!(self.peek(), Token::Join) {
21538 return Err(self
21539 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21540 }
21541 self.advance();
21542 JoinKind::Cross
21543 }
21544 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21545 Token::Right => {
21546 self.advance();
21547 if matches!(self.peek(), Token::Outer) {
21548 self.advance();
21549 }
21550 if !matches!(self.peek(), Token::Join) {
21551 return Err(self.err(format!(
21552 "expected JOIN after RIGHT [OUTER], got {:?}",
21553 self.peek()
21554 )));
21555 }
21556 self.advance();
21557 JoinKind::Right
21558 }
21559 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21560 Token::Full => {
21561 self.advance();
21562 if matches!(self.peek(), Token::Outer) {
21563 self.advance();
21564 }
21565 if !matches!(self.peek(), Token::Join) {
21566 return Err(self.err(format!(
21567 "expected JOIN after FULL [OUTER], got {:?}",
21568 self.peek()
21569 )));
21570 }
21571 self.advance();
21572 JoinKind::FullOuter
21573 }
21574 Token::Join => {
21575 self.advance();
21576 JoinKind::Inner
21577 }
21578 _ => break,
21579 };
21580 let table = self.parse_table_ref()?;
21581 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21582 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21583 // where prev_table is the most-recent left-side table
21584 // (the previous join's table if any, else the FROM primary).
21585 // PG semantics around column merging are richer (USING'd
21586 // cols become deduplicated single output columns); for
21587 // sugar purposes the predicate-only form covers the
21588 // baseline corpus shape and chained `… JOIN x USING (k)
21589 // JOIN y USING (k)` calls.
21590 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21591 // common columns resolve at execution time.
21592 if natural {
21593 joins.push(FromJoin {
21594 kind,
21595 table,
21596 on: None,
21597 using_cols: None,
21598 natural: true,
21599 });
21600 continue;
21601 }
21602 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21603 // v7.37.16 — capture the USING column list (in addition to
21604 // the ON desugar below) so the executor can perform PG's
21605 // column-merge on the output side.
21606 let mut using_cols: Option<Vec<String>> = None;
21607 let on = if matches!(self.peek(), Token::On) {
21608 self.advance();
21609 Some(self.parse_expr(0)?)
21610 } else if using_match {
21611 self.advance();
21612 if !matches!(self.peek(), Token::LParen) {
21613 return Err(
21614 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21615 );
21616 }
21617 self.advance();
21618 let mut cols: Vec<String> = Vec::new();
21619 loop {
21620 match self.peek().clone() {
21621 Token::Ident(s) | Token::QuotedIdent(s) => {
21622 self.advance();
21623 cols.push(s);
21624 }
21625 other => {
21626 return Err(self.err(format!(
21627 "expected column name inside USING (…), got {other:?}"
21628 )));
21629 }
21630 }
21631 match self.peek() {
21632 Token::Comma => {
21633 self.advance();
21634 continue;
21635 }
21636 Token::RParen => {
21637 self.advance();
21638 break;
21639 }
21640 other => {
21641 return Err(self.err(format!(
21642 "expected ',' or ')' inside USING (…), got {other:?}"
21643 )));
21644 }
21645 }
21646 }
21647 if cols.is_empty() {
21648 return Err(self.err("USING (…) requires at least one column".to_string()));
21649 }
21650 using_cols = Some(cols.clone());
21651 // Pick the left-side alias: prev join's table if any,
21652 // else FROM primary. Use alias when present, else
21653 // table name (PG-equivalent qualifier).
21654 let left_qual: String = joins
21655 .last()
21656 .map(|j| {
21657 j.table
21658 .alias
21659 .clone()
21660 .unwrap_or_else(|| j.table.name.clone())
21661 })
21662 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21663 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21664 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21665 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21666 qualifier: Some(left_qual.clone()),
21667 name: c.clone(),
21668 })),
21669 op: crate::ast::BinOp::Eq,
21670 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21671 qualifier: Some(right_qual.clone()),
21672 name: c,
21673 })),
21674 });
21675 let first = iter.next().expect("at least one col");
21676 Some(iter.fold(first, |acc, pred| Expr::Binary {
21677 lhs: alloc::boxed::Box::new(acc),
21678 op: crate::ast::BinOp::And,
21679 rhs: alloc::boxed::Box::new(pred),
21680 }))
21681 } else if kind == JoinKind::Cross {
21682 None
21683 } else {
21684 return Err(self.err(format!(
21685 "expected ON or USING after {:?} JOIN, got {:?}",
21686 kind,
21687 self.peek()
21688 )));
21689 };
21690 joins.push(FromJoin {
21691 kind,
21692 table,
21693 on,
21694 using_cols,
21695 natural: false,
21696 });
21697 }
21698 Ok(joins)
21699 }
21700
21701 /// Optional alias after an expression or table:
21702 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21703 /// accepted (PG-style implicit alias). Returns `None` if the next token
21704 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21705 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21706 if matches!(self.peek(), Token::As) {
21707 self.advance();
21708 // v7.39 (round 340, V56) — after AS the next token MUST be an
21709 // identifier. This used to return None and "let the caller
21710 // surface the error on the next expectation", but when AS is
21711 // the LAST token there is no next expectation: `SELECT 1 AS`
21712 // parsed clean and silently dropped the alias. PG rejects it.
21713 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21714 return self.expect_ident_like().map(Some);
21715 }
21716 return Err(self.err(alloc::format!(
21717 "expected an alias after AS, got {:?}",
21718 self.peek()
21719 )));
21720 }
21721 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21722 // grammar reserves a long list of follow-keywords from the
21723 // alias slot. SPG's bareword approximation: skip a small
21724 // set of idents that would otherwise be swallowed as the
21725 // table alias and break trailing clauses like CREATE
21726 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21727 // CONFLICT WHERE shapes.
21728 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21729 if is_alias_stopword(s) {
21730 return Ok(None);
21731 }
21732 return Ok(self.expect_ident_like().ok());
21733 }
21734 Ok(None)
21735 }
21736
21737 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21738 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21739 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21740 // error beats a stack overflow (an overflow aborts the
21741 // embedding host process).
21742 self.enter_nested()?;
21743 let r = self.parse_expr_inner(min_prec);
21744 self.nest_depth -= 1;
21745 r
21746 }
21747
21748 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21749 /// When the upcoming tokens form one, return the underlying
21750 /// operator token and the position just past the closing paren
21751 /// so the binary loop can dispatch on the plain operator.
21752 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21753 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21754 return None;
21755 }
21756 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21757 return None;
21758 }
21759 let mut i = self.pos + 2;
21760 // Optional schema qualifier (pg_catalog.<op> etc.).
21761 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21762 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21763 {
21764 i += 2;
21765 }
21766 let op_tok = self.tokens.get(i)?.clone();
21767 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21768 return None;
21769 }
21770 Some((i + 2, op_tok))
21771 }
21772
21773 /// PG operator symbols that lower onto function calls in
21774 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21775 /// family → regexp_like, comparison rung), `^@` (starts_with,
21776 /// comparison rung), `^` (power, tighter than `*`), `#`
21777 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21778 /// subset of the OR bits so the subtraction never borrows).
21779 fn try_symbol_operator(
21780 &mut self,
21781 lhs: &Expr,
21782 min_prec: u8,
21783 ) -> Result<Option<Expr>, ParseError> {
21784 enum Sym {
21785 Regex { ci: bool, negated: bool },
21786 Like { ci: bool, negated: bool },
21787 StartsWith,
21788 Power,
21789 Xor,
21790 RangeAdjacent,
21791 }
21792 // v7.39 (IS-precedence knife) — the low-precedence postfix
21793 // predicates ride this existing leaf call (zero new frame slots
21794 // on the nesting chain).
21795 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21796 return Ok(Some(e));
21797 }
21798 let (sym, prec): (Sym, u8) = match self.peek() {
21799 Token::Tilde => (
21800 Sym::Regex {
21801 ci: false,
21802 negated: false,
21803 },
21804 5,
21805 ),
21806 Token::TildeStar => (
21807 Sym::Regex {
21808 ci: true,
21809 negated: false,
21810 },
21811 5,
21812 ),
21813 Token::NotTilde => (
21814 Sym::Regex {
21815 ci: false,
21816 negated: true,
21817 },
21818 5,
21819 ),
21820 Token::NotTildeStar => (
21821 Sym::Regex {
21822 ci: true,
21823 negated: true,
21824 },
21825 5,
21826 ),
21827 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21828 Token::DoubleTilde => (
21829 Sym::Like {
21830 ci: false,
21831 negated: false,
21832 },
21833 5,
21834 ),
21835 Token::DoubleTildeStar => (
21836 Sym::Like {
21837 ci: true,
21838 negated: false,
21839 },
21840 5,
21841 ),
21842 Token::NotDoubleTilde => (
21843 Sym::Like {
21844 ci: false,
21845 negated: true,
21846 },
21847 5,
21848 ),
21849 Token::NotDoubleTildeStar => (
21850 Sym::Like {
21851 ci: true,
21852 negated: true,
21853 },
21854 5,
21855 ),
21856 Token::CaretAt => (Sym::StartsWith, 5),
21857 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21858 // tighter than `* / & |`, which the prec-9 rung preserves —
21859 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21860 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21861 Token::Caret => (Sym::Power, 9),
21862 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21863 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21864 Token::Hash => (Sym::Xor, 6),
21865 Token::Adjacent => (Sym::RangeAdjacent, 5),
21866 _ => return Ok(None),
21867 };
21868 if prec < min_prec {
21869 return Ok(None);
21870 }
21871 self.advance();
21872 let rhs = self.parse_expr(prec + 1)?;
21873 let out = match sym {
21874 Sym::Regex { ci, negated } => {
21875 let mut args = alloc::vec![lhs.clone(), rhs];
21876 if ci {
21877 args.push(Expr::Literal(Literal::String(String::from("i"))));
21878 }
21879 maybe_not(
21880 Expr::FunctionCall {
21881 name: String::from("regexp_like"),
21882 args,
21883 },
21884 negated,
21885 )
21886 }
21887 Sym::Like { ci, negated } => Expr::Like {
21888 expr: alloc::boxed::Box::new(lhs.clone()),
21889 pattern: alloc::boxed::Box::new(rhs),
21890 negated,
21891 case_insensitive: ci,
21892 },
21893 Sym::StartsWith => Expr::FunctionCall {
21894 name: String::from("starts_with"),
21895 args: alloc::vec![lhs.clone(), rhs],
21896 },
21897 Sym::Power => Expr::FunctionCall {
21898 name: String::from("power"),
21899 args: alloc::vec![lhs.clone(), rhs],
21900 },
21901 // `#` bitwise XOR — a real operator now (was desugared to
21902 // `(a|b)-(a&b)`, algebraically identical for integers but
21903 // undefined for bit strings; the direct op handles both).
21904 Sym::Xor => Expr::Binary {
21905 lhs: Box::new(lhs.clone()),
21906 op: BinOp::BitXor,
21907 rhs: Box::new(rhs),
21908 },
21909 // range `-|-` "is adjacent to" — lowered to a catalog function.
21910 Sym::RangeAdjacent => Expr::FunctionCall {
21911 name: String::from("range_adjacent"),
21912 args: alloc::vec![lhs.clone(), rhs],
21913 },
21914 };
21915 Ok(Some(out))
21916 }
21917
21918 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21919 /// predicates, moved out of the tight postfix-cast loop: PG binds
21920 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21921 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21922 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21923 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21924 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21925 /// when nothing at this position belongs to the family. Out-of-line
21926 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21927 /// chain that MAX_NEST_DEPTH is tuned against.
21928 #[inline(never)]
21929 fn parse_postfix_predicate(
21930 &mut self,
21931 lhs: &Expr,
21932 min_prec: u8,
21933 ) -> Result<Option<Expr>, ParseError> {
21934 // Reached through try_symbol_operator (an existing leaf call of
21935 // the binary loop) so NO new stack slots land on the per-nesting
21936 // frame chain; the lhs clones only when a predicate actually
21937 // consumes it.
21938 match self.peek() {
21939 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21940 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21941 Token::Is if min_prec <= 4 => {}
21942 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21943 Token::Not
21944 if min_prec <= 5
21945 && matches!(
21946 self.tokens.get(self.pos + 1),
21947 Some(Token::Between | Token::In | Token::Like)
21948 ) => {}
21949 Token::Not | Token::Ident(_)
21950 if min_prec <= 5
21951 && (matches!(self.peek(), Token::Ident(s)
21952 if s.eq_ignore_ascii_case("ilike")
21953 || (self.mysql_dialect
21954 && (s.eq_ignore_ascii_case("regexp")
21955 || s.eq_ignore_ascii_case("rlike")))
21956 || (s.eq_ignore_ascii_case("similar")
21957 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21958 || (matches!(self.peek(), Token::Not)
21959 && matches!(self.tokens.get(self.pos + 1),
21960 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21961 || (self.mysql_dialect
21962 && (s.eq_ignore_ascii_case("regexp")
21963 || s.eq_ignore_ascii_case("rlike")))
21964 || s.eq_ignore_ascii_case("similar")))) => {}
21965 _ => return Ok(None),
21966 }
21967 let mut expr = lhs.clone();
21968 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21969 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21970 if min_prec <= 4 {
21971 if matches!(self.peek(), Token::Is) {
21972 self.advance();
21973 let negated = if matches!(self.peek(), Token::Not) {
21974 self.advance();
21975 true
21976 } else {
21977 false
21978 };
21979 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21980 // mailrs pg_dump.
21981 if matches!(self.peek(), Token::Distinct) {
21982 self.advance();
21983 if !matches!(self.peek(), Token::From) {
21984 return Err(self.err(format!(
21985 "expected FROM after IS{} DISTINCT, got {:?}",
21986 if negated { " NOT" } else { "" },
21987 self.peek()
21988 )));
21989 }
21990 self.advance();
21991 // Right-hand side: parse at the same precedence
21992 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21993 // groups as `x IS DISTINCT FROM (a + b)`.
21994 let rhs = self.parse_expr(5)?;
21995 let op = if negated {
21996 BinOp::IsNotDistinctFrom
21997 } else {
21998 BinOp::IsDistinctFrom
21999 };
22000 expr = Expr::Binary {
22001 op,
22002 lhs: Box::new(expr),
22003 rhs: Box::new(rhs),
22004 };
22005 {
22006 return Ok(Some(expr));
22007 }
22008 }
22009 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22010 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22011 // Lowers onto pg_is_json(x, kind); NOT wraps the
22012 // call in a logical negation.
22013 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22014 if s.eq_ignore_ascii_case("json"))
22015 {
22016 self.advance(); // JSON
22017 let kind = match self.peek() {
22018 Token::Ident(s) | Token::QuotedIdent(s)
22019 if matches!(
22020 s.to_ascii_lowercase().as_str(),
22021 "value" | "object" | "array" | "scalar"
22022 ) =>
22023 {
22024 let k = s.to_ascii_lowercase();
22025 self.advance();
22026 k
22027 }
22028 _ => "value".to_string(),
22029 };
22030 let call = Expr::FunctionCall {
22031 name: "pg_is_json".to_string(),
22032 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22033 };
22034 expr = if negated {
22035 Expr::Unary {
22036 op: UnOp::Not,
22037 expr: Box::new(call),
22038 }
22039 } else {
22040 call
22041 };
22042 {
22043 return Ok(Some(expr));
22044 }
22045 }
22046 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22047 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22048 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22049 {
22050 let form_kw = match self.peek() {
22051 Token::Ident(s) | Token::QuotedIdent(s)
22052 if matches!(
22053 s.to_ascii_uppercase().as_str(),
22054 "NFC" | "NFD" | "NFKC" | "NFKD"
22055 ) && matches!(
22056 self.tokens.get(self.pos + 1),
22057 Some(Token::Ident(n) | Token::QuotedIdent(n))
22058 if n.eq_ignore_ascii_case("normalized")
22059 ) =>
22060 {
22061 Some(s.to_ascii_uppercase())
22062 }
22063 _ => None,
22064 };
22065 let bare_normalized = form_kw.is_none()
22066 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22067 if s.eq_ignore_ascii_case("normalized"));
22068 if form_kw.is_some() || bare_normalized {
22069 if form_kw.is_some() {
22070 self.advance(); // form keyword
22071 }
22072 self.advance(); // NORMALIZED
22073 let mut args = alloc::vec![expr];
22074 if let Some(f) = form_kw {
22075 args.push(Expr::Literal(Literal::String(f)));
22076 }
22077 let call = Expr::FunctionCall {
22078 name: "is_normalized".to_string(),
22079 args,
22080 };
22081 expr = if negated {
22082 Expr::Unary {
22083 op: UnOp::Not,
22084 expr: Box::new(call),
22085 }
22086 } else {
22087 call
22088 };
22089 {
22090 return Ok(Some(expr));
22091 }
22092 }
22093 }
22094 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22095 // three-valued boolean tests. IS TRUE/FALSE never
22096 // return NULL, so they lower to CASE forms whose
22097 // ELSE catches the NULL branch; IS UNKNOWN on a
22098 // boolean is exactly IS NULL.
22099 if matches!(self.peek(), Token::True | Token::False)
22100 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22101 {
22102 let tok = self.advance();
22103 let test = match tok {
22104 Token::True => Some(true),
22105 Token::False => Some(false),
22106 _ => None, // UNKNOWN
22107 };
22108 // v7.39 (round 328, V45) — kept as what the user
22109 // wrote. These used to be lowered here into `CASE` /
22110 // `IS NULL`; the semantics were right but the AST no
22111 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22112 // was echoed back as
22113 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22114 expr = Expr::BoolTest {
22115 expr: Box::new(expr),
22116 value: test,
22117 negated,
22118 };
22119 {
22120 return Ok(Some(expr));
22121 }
22122 }
22123 if !matches!(self.peek(), Token::Null) {
22124 return Err(self.err(format!(
22125 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22126 if negated { " NOT" } else { "" },
22127 self.peek()
22128 )));
22129 }
22130 self.advance();
22131 expr = Expr::IsNull {
22132 expr: Box::new(expr),
22133 negated,
22134 };
22135 {
22136 return Ok(Some(expr));
22137 }
22138 }
22139 }
22140 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22141 if min_prec <= 5 {
22142 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22143 // Look one token ahead so a stray `NOT` not followed by any of
22144 // these flows through to the early return below untouched.
22145 let negated = if matches!(self.peek(), Token::Not) {
22146 let next = self.tokens.get(self.pos + 1);
22147 matches!(next, Some(Token::Between | Token::In | Token::Like))
22148 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22149 || (self.mysql_dialect
22150 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22151 || s.eq_ignore_ascii_case("similar"))
22152 } else {
22153 false
22154 };
22155 if negated {
22156 self.advance();
22157 }
22158 if matches!(self.peek(), Token::Between) {
22159 expr = self.parse_between_tail(expr, negated)?;
22160 {
22161 return Ok(Some(expr));
22162 }
22163 }
22164 if matches!(self.peek(), Token::In) {
22165 if self.suppress_in_tail && !negated {
22166 // POSITION(sub IN str) — IN belongs to the
22167 // enclosing function syntax; stop here.
22168 {
22169 return Ok(None);
22170 }
22171 }
22172 expr = self.parse_in_tail(expr, negated)?;
22173 {
22174 return Ok(Some(expr));
22175 }
22176 }
22177 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22178 // lowers onto the internal __similar_to(expr, pat[, esc]) call
22179 // (the SQL→regex transform runs inside, in the backtracking-
22180 // friendly shape SPG's matcher needs).
22181 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22182 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22183 {
22184 self.advance(); // SIMILAR
22185 self.advance(); // TO
22186 let pattern = self.parse_expr(6)?;
22187 let mut args = alloc::vec![expr, pattern];
22188 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22189 self.advance();
22190 args.push(self.parse_expr(6)?);
22191 }
22192 let call = Expr::FunctionCall {
22193 name: "__similar_to".to_string(),
22194 args,
22195 };
22196 expr = maybe_not(call, negated);
22197 {
22198 return Ok(Some(expr));
22199 }
22200 }
22201 if matches!(self.peek(), Token::Like) {
22202 self.advance();
22203 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22204 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22205 expr = q;
22206 {
22207 return Ok(Some(expr));
22208 }
22209 }
22210 // Pattern at the same precedence as other comparison RHSes —
22211 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22212 let mut pattern = self.parse_expr(6)?;
22213 // `ESCAPE 'c'` — rewrite a literal pattern to the
22214 // default backslash escape at parse time. Custom
22215 // escapes on non-literal patterns would need
22216 // matcher support; error honestly.
22217 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22218 self.advance();
22219 let esc = self.parse_expr(6)?;
22220 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22221 }
22222 expr = Expr::Like {
22223 expr: Box::new(expr),
22224 pattern: Box::new(pattern),
22225 negated,
22226 case_insensitive: false,
22227 };
22228 {
22229 return Ok(Some(expr));
22230 }
22231 }
22232 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22233 // keyword reaches us as a plain identifier.
22234 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22235 self.advance();
22236 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22237 expr = q;
22238 {
22239 return Ok(Some(expr));
22240 }
22241 }
22242 let pattern = self.parse_expr(6)?;
22243 expr = Expr::Like {
22244 expr: Box::new(expr),
22245 pattern: Box::new(pattern),
22246 negated,
22247 case_insensitive: true,
22248 };
22249 {
22250 return Ok(Some(expr));
22251 }
22252 }
22253 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22254 // operator (RLIKE is the alias). It is a keyword, not `~`, and
22255 // matches case-insensitively under the default collation, so it
22256 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22257 // `~*` operator uses, wrapped in NOT when negated.
22258 if self.mysql_dialect
22259 && matches!(self.peek(), Token::Ident(s)
22260 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22261 {
22262 self.advance();
22263 let pattern = self.parse_expr(6)?;
22264 let call = Expr::FunctionCall {
22265 name: String::from("regexp_like"),
22266 args: alloc::vec![
22267 expr,
22268 pattern,
22269 Expr::Literal(Literal::String(String::from("i"))),
22270 ],
22271 };
22272 return Ok(Some(maybe_not(call, negated)));
22273 }
22274 }
22275 let _ = expr;
22276 Ok(None)
22277 }
22278
22279 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22280 let mut lhs = self.parse_unary()?;
22281 let mut chain_len = 0usize;
22282 loop {
22283 // OPERATOR([schema.]op) reduces to its underlying
22284 // operator token before the normal dispatch.
22285 let explicit = self.peek_explicit_operator();
22286 let dispatch = match &explicit {
22287 Some((_, tok)) => self.binop_here(tok),
22288 None => self.binop_here(self.peek()),
22289 };
22290 let Some((op, prec)) = dispatch else {
22291 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22292 // of the symbol family. `binop_here` answers None for them
22293 // because they lower onto function calls rather than a
22294 // BinOp, and the fallback below reads `self.peek()` — the
22295 // word OPERATOR, not the operator. `pg_dump` writes every
22296 // catalog predicate this way, so its first query failed
22297 // and no dump ran:
22298 //
22299 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22300 //
22301 // Collapsing the wrapper to the operator it names puts the
22302 // token where the fallback already looks.
22303 if let Some((next, op_tok)) = explicit {
22304 self.tokens.splice(self.pos..next, [op_tok]);
22305 }
22306 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22307 lhs = e;
22308 chain_len += 1;
22309 if chain_len > MAX_BINARY_CHAIN {
22310 return Err(self.err(alloc::format!(
22311 "more than {MAX_BINARY_CHAIN} chained binary operators"
22312 )));
22313 }
22314 continue;
22315 }
22316 break;
22317 };
22318 if prec < min_prec {
22319 break;
22320 }
22321 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22322 // iteratively but evaluates and drops recursively;
22323 // depth beyond the budget overflows worker stacks.
22324 chain_len += 1;
22325 if chain_len > MAX_BINARY_CHAIN {
22326 return Err(self.err(alloc::format!(
22327 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22328 )));
22329 }
22330 match explicit {
22331 Some((end_pos, _)) => self.pos = end_pos,
22332 None => {
22333 self.advance();
22334 }
22335 }
22336 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22337 // ANY is a bare ident; ALL is a reserved Token. Both
22338 // require an immediate `(` to disambiguate from
22339 // identifier columns named `any` / `all`.
22340 let any_kind = match self.peek() {
22341 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22342 Some(false)
22343 }
22344 Token::Ident(s) | Token::QuotedIdent(s)
22345 if (s.eq_ignore_ascii_case("any")
22346 || s.eq_ignore_ascii_case("some")
22347 || s.eq_ignore_ascii_case("all"))
22348 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22349 {
22350 Some(!s.eq_ignore_ascii_case("all"))
22351 }
22352 _ => None,
22353 };
22354 if let Some(is_any) = any_kind {
22355 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22356 continue;
22357 }
22358 let rhs = self.parse_expr(prec + 1)?;
22359 lhs = Expr::Binary {
22360 lhs: Box::new(lhs),
22361 op,
22362 rhs: Box::new(rhs),
22363 };
22364 }
22365 Ok(lhs)
22366 }
22367
22368 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22369 /// and the array form.
22370 ///
22371 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22372 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22373 /// this block's `Expr` temporaries and four `format!` sites slots in
22374 /// that frame on every level of `((((1))))`, which never reaches it.
22375 #[inline(never)]
22376 fn parse_any_all_rhs(
22377 &mut self,
22378 lhs: Expr,
22379 op: BinOp,
22380 is_any: bool,
22381 ) -> Result<Expr, ParseError> {
22382 self.advance(); // ident
22383 self.advance(); // (
22384 // `x op ANY (SELECT …)` — the quantified-subquery
22385 // form. `= ANY` is exactly IN; the other operators
22386 // lower onto EXISTS over the subquery as a derived
22387 // table, comparing against its single projection
22388 // aliased __v (x's columns resolve correlated).
22389 // ALL is the negated-EXISTS complement; a NULL
22390 // element makes PG return NULL where this lowering
22391 // returns true — the NOT NULL column case (the
22392 // practical one) is exact.
22393 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22394 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22395 // legal PG too (round-151 sibling). Out-of-line
22396 // (#[inline(never)] helper) — this sits on
22397 // parse_expr's recursive frame and the two-armed
22398 // SELECT temporary blew the nesting-budget stack.
22399 let mut sub = self.parse_any_all_select_body()?;
22400 if !matches!(self.peek(), Token::RParen) {
22401 return Err(self.err(alloc::format!(
22402 "expected ')' after ANY/ALL subquery, got {:?}",
22403 self.peek()
22404 )));
22405 }
22406 self.advance();
22407 if sub.items.len() != 1 {
22408 return Err(self.err(alloc::format!(
22409 "ANY/ALL subquery must return one column, got {}",
22410 sub.items.len()
22411 )));
22412 }
22413 if is_any && matches!(op, BinOp::Eq) {
22414 return Ok(Expr::InSubquery {
22415 expr: Box::new(lhs),
22416 subquery: Box::new(sub),
22417 negated: false,
22418 });
22419 }
22420 // The engine's subquery resolvers materialise
22421 // the single-column result into an ARRAY the
22422 // existing AnyAll three-valued eval consumes.
22423 return Ok(Expr::AnyAll {
22424 expr: Box::new(lhs),
22425 op,
22426 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22427 is_any,
22428 });
22429 }
22430 let arr = self.parse_expr(0)?;
22431 if !matches!(self.peek(), Token::RParen) {
22432 return Err(self.err(alloc::format!(
22433 "expected ')' after ANY/ALL argument, got {:?}",
22434 self.peek()
22435 )));
22436 }
22437 self.advance();
22438 Ok(Expr::AnyAll {
22439 expr: Box::new(lhs),
22440 op,
22441 array: Box::new(arr),
22442 is_any,
22443 })
22444 }
22445
22446 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22447 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22448 #[inline(never)]
22449 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22450 self.advance();
22451 let e = self.parse_expr(9)?;
22452 Ok(build_center_call(e))
22453 }
22454
22455 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22456 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22457 /// unary minus.
22458 ///
22459 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22460 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22461 /// the Expr-sized local stays out of that frame.
22462 #[inline(never)]
22463 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22464 self.advance();
22465 let e = self.parse_expr(9)?;
22466 Ok(Expr::FunctionCall {
22467 name: alloc::string::String::from(name),
22468 args: alloc::vec![e],
22469 })
22470 }
22471
22472 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22473 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22474 #[inline(never)]
22475 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22476 self.advance();
22477 let e = self.parse_expr(9)?;
22478 Ok(Expr::FunctionCall {
22479 name: alloc::string::String::from(if vertical {
22480 "isvertical"
22481 } else {
22482 "ishorizontal"
22483 }),
22484 args: alloc::vec![e],
22485 })
22486 }
22487
22488 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22489 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22490 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22491 #[inline(never)]
22492 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22493 self.advance();
22494 let e = self.parse_expr(9)?;
22495 Ok(Expr::Cast {
22496 expr: Box::new(e),
22497 target: CastTarget::Named("binary".to_string()),
22498 })
22499 }
22500
22501 /// The prefix operators that share one shape: take the token, parse
22502 /// an operand at `prec`, wrap it.
22503 ///
22504 /// `#[inline(never)]`, and one function instead of five arms, for the
22505 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22506 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22507 /// debug build gives EVERY arm's locals a slot in the frame, whichever
22508 /// arm runs. `((((1))))` reaches none of these arms and was carrying
22509 /// five `Expr`-sized locals per level for them anyway.
22510 #[inline(never)]
22511 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22512 self.advance();
22513 let e = self.parse_expr(prec)?;
22514 Ok(Expr::Unary {
22515 op,
22516 expr: Box::new(e),
22517 })
22518 }
22519
22520 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22521 /// and separate from it because of the literal folding below and the
22522 /// `format!` temporaries that folding needs.
22523 #[inline(never)]
22524 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22525 self.advance();
22526 // v7.39 (round 549) — fold the sign into an integer literal that
22527 // only fits once it is negative.
22528 //
22529 // `9223372036854775808` is one past i64::MAX, so the lexer hands
22530 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22531 // folds the sign first, so `-9223372036854775808` is a bigint
22532 // there — and `-9223372036854775808 - 1` raises "bigint out of
22533 // range" where SPG quietly answered -9223372036854775809, a value
22534 // no bigint can hold. The arithmetic itself was already checked;
22535 // only the literal's type was wrong.
22536 if let Token::Numeric(lit) = self.peek()
22537 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22538 {
22539 self.advance();
22540 return Ok(Expr::Literal(Literal::Integer(folded)));
22541 }
22542 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22543 // `<->` slotted into 5 and arithmetic shifted up).
22544 let e = self.parse_expr(9)?;
22545 Ok(Expr::Unary {
22546 op: UnOp::Neg,
22547 expr: Box::new(e),
22548 })
22549 }
22550
22551 /// tsquery `!!` prefix negation, lowered to the catalog function.
22552 /// Binds like unary minus. Out-of-line for the frame reason on
22553 /// `parse_unary_op`.
22554 #[inline(never)]
22555 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22556 self.advance();
22557 let e = self.parse_expr(9)?;
22558 Ok(Expr::FunctionCall {
22559 name: String::from("tsquery_not"),
22560 args: alloc::vec![e],
22561 })
22562 }
22563
22564 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22565 match self.peek() {
22566 // NOT binds tighter than AND / XOR / OR but looser than
22567 // comparisons — its operand takes everything ≥ the comparison
22568 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22569 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22570 // was rung 3, behaviour-identical when 3 was unused; AND now
22571 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22572 Token::Not => self.parse_unary_op(UnOp::Not, 4),
22573 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22574 // The body is out-of-line: `parse_unary` is one of the three
22575 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22576 // inline arm here overflowed the native stack in
22577 // `nesting_budget_errors_cleanly` — the guard test caught it,
22578 // exactly as the eval-side cliff did in rounds 346 and 351.
22579 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22580 self.parse_binary_prefix()
22581 }
22582 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22583 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22584 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22585 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22586 Token::Minus => self.parse_prefix_minus(),
22587 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22588 // worked only because the lexer reads it as one signed literal;
22589 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22590 // PG18 and MariaDB take all of them. Binds like unary minus.
22591 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22592 // Bitwise NOT binds like unary minus.
22593 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22594 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22595 // "center of" operator; desugars to center(x). The whole arm
22596 // is out-of-line: parse_unary sits on the per-nesting-level
22597 // frame chain that MAX_NEST_DEPTH is tuned against, so no
22598 // Expr-sized local may live in this frame.
22599 Token::TsMatch => self.parse_prefix_center(),
22600 // v7.39 (round 508) — the prefix operators that are named
22601 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22602 // is length. Out-of-line for the same nesting-frame reason as
22603 // parse_prefix_center — parse_unary sits on the recursive cycle
22604 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22605 // live in this frame.
22606 Token::At => self.parse_prefix_call("abs"),
22607 Token::Hash => self.parse_prefix_call("npoints"),
22608 Token::AtMinusAt => self.parse_prefix_call("length"),
22609 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22610 // "is horizontal" (lseg / line); desugars to the existing
22611 // isvertical()/ishorizontal() functions. Out-of-line for the
22612 // same nesting-frame reason as parse_prefix_center.
22613 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22614 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22615 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22616 _ => self.parse_atom(),
22617 }
22618 }
22619
22620 /// Parse a parenthesised scalar subquery body after the caller has consumed
22621 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22622 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22623 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22624 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22625 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22626 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22627 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22628 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22629 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22630 /// tips the deep-nesting test into a stack overflow).
22631 #[inline(never)]
22632 fn array_subquery_ahead(&self) -> bool {
22633 if !matches!(self.peek(), Token::LParen) {
22634 return false;
22635 }
22636 matches!(
22637 self.tokens.get(self.pos + 1),
22638 Some(Token::Select | Token::Values)
22639 ) || matches!(
22640 self.tokens.get(self.pos + 1),
22641 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22642 )
22643 }
22644
22645 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22646 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22647 /// locals stay off parse_atom's recursive frame (round 105).
22648 #[inline(never)]
22649 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22650 self.advance(); // consume `[`
22651 let mut items: Vec<Expr> = Vec::new();
22652 if !matches!(self.peek(), Token::RBracket) {
22653 loop {
22654 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22655 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22656 if matches!(self.peek(), Token::LBracket) {
22657 items.push(self.parse_array_bracket_body()?);
22658 } else {
22659 items.push(self.parse_expr(0)?);
22660 }
22661 match self.peek() {
22662 Token::Comma => {
22663 self.advance();
22664 }
22665 Token::RBracket => break,
22666 other => {
22667 return Err(self.err(alloc::format!(
22668 "expected ',' or ']' in ARRAY literal, got {other:?}"
22669 )));
22670 }
22671 }
22672 }
22673 }
22674 self.advance(); // consume `]`
22675 Ok(Expr::Array(items))
22676 }
22677
22678 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22679 /// is already consumed; the current token is `(`. Desugars to a scalar
22680 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22681 /// the subquery's single-column rows in order — reusing the existing
22682 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22683 /// keeps the large `Statement` local off parse_atom's recursive frame.
22684 #[inline(never)]
22685 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22686 self.advance(); // consume `(`
22687 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22688 if w.eq_ignore_ascii_case("with"));
22689 let sub = if is_with {
22690 self.advance(); // WITH
22691 self.parse_with_cte_then_select()?
22692 } else {
22693 self.parse_select_stmt()?
22694 };
22695 if !matches!(self.peek(), Token::RParen) {
22696 return Err(self.err(alloc::format!(
22697 "expected ')' to close ARRAY(subquery), got {:?}",
22698 self.peek()
22699 )));
22700 }
22701 self.advance(); // consume `)`
22702 // Reuse the parser to build the array_agg wrapper from the subquery's
22703 // canonical text — avoids hand-constructing the derived-table AST.
22704 let wrapper = alloc::format!(
22705 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22706 );
22707 let stmt = parse_statement(&wrapper)
22708 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22709 let Statement::Select(sel) = stmt else {
22710 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22711 };
22712 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22713 }
22714
22715 #[inline(never)]
22716 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22717 let inner = if is_with {
22718 self.advance(); // WITH
22719 self.parse_with_cte_then_select()?
22720 } else {
22721 self.parse_select_stmt()?
22722 };
22723 match self.advance() {
22724 Token::RParen => {
22725 let Statement::Select(s) = inner else {
22726 return Err(ParseError {
22727 message: "scalar subquery body must be a SELECT".into(),
22728 token_pos: self.consumed_pos(),
22729 });
22730 };
22731 Ok(Expr::ScalarSubquery(Box::new(s)))
22732 }
22733 other => Err(ParseError {
22734 message: format!("expected ')' after scalar subquery, got {other:?}"),
22735 token_pos: self.consumed_pos(),
22736 }),
22737 }
22738 }
22739
22740 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22741 /// literals. The lexer splits them into an ident + string; recombine
22742 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22743 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22744 /// frame for the `body` / `bits` strings and their char loops (the
22745 /// round-367 frame cliff, M20).
22746 #[inline(never)]
22747 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22748 let is_hex = match self.peek() {
22749 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22750 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22751 _ => return None,
22752 };
22753 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22754 return None;
22755 }
22756 // v7.39.3 — where the LITERAL starts, because the errors below
22757 // are about the literal and both engines point at it. `err`
22758 // reports the CURRENT token, which by then is the one after the
22759 // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22760 // `near '…'` snippet — which runs from the reported position to
22761 // the end — came out empty where MySQL 9.7.2 says `near
22762 // 'x'123''`.
22763 let lit_pos = self.pos;
22764 self.advance();
22765 let Token::String(body) = self.advance() else {
22766 unreachable!("guarded above");
22767 };
22768 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22769 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22770 // (hex pairs, even count required — MariaDB errors on an odd
22771 // count); `b'1010'` packs its bits big-endian, left-padded to a
22772 // byte. Lower both onto the bytea cast.
22773 if self.mysql_dialect {
22774 if is_hex {
22775 if body.len() % 2 == 1 {
22776 return Some(Err(self.err_at(
22777 lit_pos,
22778 alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22779 )));
22780 }
22781 for c in body.chars() {
22782 if !c.is_ascii_hexdigit() {
22783 return Some(Err(self.err_at(
22784 lit_pos,
22785 alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22786 )));
22787 }
22788 }
22789 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22790 }
22791 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22792 return Some(Err(self.err_at(
22793 lit_pos,
22794 alloc::format!("invalid binary digit {bad:?} in b'…'"),
22795 )));
22796 }
22797 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22798 }
22799 let bits = if is_hex {
22800 let mut out = String::with_capacity(body.len() * 4);
22801 for c in body.chars() {
22802 let Some(d) = c.to_digit(16) else {
22803 // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22804 // own quoting: `"g" is not a valid hexadecimal
22805 // digit` (measured, with the caret on the literal).
22806 return Some(Err(self.err_at(
22807 lit_pos,
22808 alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22809 )));
22810 };
22811 out.push_str(&alloc::format!("{d:04b}"));
22812 }
22813 out
22814 } else {
22815 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22816 return Some(Err(self.err_at(
22817 lit_pos,
22818 alloc::format!("\"{bad}\" is not a valid binary digit"),
22819 )));
22820 }
22821 body
22822 };
22823 // Route through the postfix-cast loop so a chained cast like
22824 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22825 // of erroring at the `::`.
22826 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22827 // literal keeps its exact length, while an explicit `::bit` cast is
22828 // bit(1) with pad/truncate semantics (PG).
22829 Some(self.finish_postfix_casts(Expr::Cast {
22830 expr: Box::new(Expr::Literal(Literal::String(bits))),
22831 target: CastTarget::Named("__bit_literal".to_string()),
22832 }))
22833 }
22834
22835 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22836 if let Some(res) = self.try_parse_bit_string_literal() {
22837 return res;
22838 }
22839 let tok_pos = self.pos;
22840 match self.advance() {
22841 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22842 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22843 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22844 // carrying the source mantissa + scale so no precision is lost. A
22845 // literal too wide for i128 falls back to double precision.
22846 // Out-of-line (#[inline(never)]) — this arm sits on the
22847 // parse_expr recursion chain; its expansion locals must not
22848 // widen the recursive frame (debug frame-cliff discipline).
22849 Token::Numeric(s) => match numeric_token_to_literal(s) {
22850 Ok(lit) => Ok(Expr::Literal(lit)),
22851 Err(msg) => Err(self.err(msg)),
22852 },
22853 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22854 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22855 // (the lexer only emits this token in the MySQL dialect). Lower
22856 // onto the existing bytea cast; out-of-line to keep this arm off
22857 // the parse recursion frame.
22858 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22859 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22860 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22861 Token::Null => Ok(Expr::Literal(Literal::Null)),
22862 // v6.1.1 — `$N` placeholder. The actual Value lookup
22863 // happens in the engine eval path against the prepared-
22864 // statement bind buffer.
22865 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22866 Token::LParen => {
22867 // v4.10: `(SELECT ...)` in expression position is a
22868 // scalar subquery; otherwise it's a parenthesised
22869 // expression. Peek for SELECT keyword to dispatch.
22870 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22871 // lexes as Ident("with") (not a reserved token). The subquery body
22872 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22873 // so its large `Statement` local stays out of parse_atom's stack
22874 // frame — parse_atom is on the recursive `((…))` cycle and the
22875 // nesting budget is tuned to its frame size).
22876 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22877 if s.eq_ignore_ascii_case("with"));
22878 if matches!(self.peek(), Token::Select) || is_with {
22879 self.parse_paren_scalar_subquery(is_with)
22880 } else {
22881 let e = self.parse_expr(0)?;
22882 // `(a, b, …)` — a row constructor. Valid only
22883 // in front of a comparison operator or [NOT]
22884 // IN; both expand at parse time (lexicographic
22885 // comparison / OR'd row equalities).
22886 if matches!(self.peek(), Token::Comma) {
22887 let mut row = alloc::vec![e];
22888 while matches!(self.peek(), Token::Comma) {
22889 self.advance();
22890 row.push(self.parse_expr(0)?);
22891 }
22892 if !matches!(self.peek(), Token::RParen) {
22893 return Err(self.err(alloc::format!(
22894 "expected ')' after row constructor, got {:?}",
22895 self.peek()
22896 )));
22897 }
22898 self.advance();
22899 // A bare `(a, b, …)` row constructor can carry postfix
22900 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22901 // early return here skips parse_atom's tail postfix
22902 // pass, so fold casts in explicitly. For the
22903 // comparison / predicate forms nothing postfix follows,
22904 // so this is a no-op.
22905 return self
22906 .parse_row_comparison_tail(row)
22907 .and_then(|e| self.finish_postfix_casts(e));
22908 }
22909 match self.advance() {
22910 Token::RParen => Ok(e),
22911 other => Err(ParseError {
22912 message: format!("expected ')', got {other:?}"),
22913 token_pos: self.consumed_pos(),
22914 }),
22915 }
22916 }
22917 }
22918 Token::LBracket => self.parse_vector_literal_body(),
22919 Token::Extract => self.parse_extract_atom(),
22920 Token::Interval => self.parse_interval_atom(),
22921 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22922 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22923 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22924 // expression position calling the PG `left(string, n)` /
22925 // `right(string, n)` function; rebuild the AST as a regular
22926 // function call so the engine's apply_function dispatch picks
22927 // it up. Delegated to a #[inline(never)] helper so its locals
22928 // don't bloat this recursive `parse_atom` frame (the nesting
22929 // budget in `enter_nested` is tuned to parse_atom's size).
22930 Token::Left if matches!(self.peek(), Token::LParen) => {
22931 self.parse_lr_string_function_call("left")
22932 }
22933 Token::Right if matches!(self.peek(), Token::LParen) => {
22934 self.parse_lr_string_function_call("right")
22935 }
22936 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22937 // token; we match on the bare ident. NOT is a token
22938 // (consumed in the comparison rung), but `EXISTS (...)`
22939 // at the top of an expression starts here.
22940 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22941 self.parse_exists_atom(false)
22942 }
22943 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22944 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22945 // CASE is a bare ident; we dispatch on lowercase match.
22946 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22947 self.parse_case_atom()
22948 }
22949 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22950 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22951 // '…'`. Lower onto the ::cast node so the existing
22952 // runtime text→date/timestamp paths do the parsing. The
22953 // string must follow immediately, else the ident stays a
22954 // plain column reference.
22955 Token::Ident(s)
22956 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22957 && matches!(self.peek(), Token::String(_)) =>
22958 {
22959 let target =
22960 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22961 let Token::String(lit) = self.advance() else {
22962 unreachable!("peek guaranteed a string token");
22963 };
22964 Ok(Expr::Cast {
22965 expr: Box::new(Expr::Literal(Literal::String(lit))),
22966 target,
22967 })
22968 }
22969 // v7.39 (round 221) — the SQL-standard long spellings:
22970 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22971 // TIME ZONE '…'`. Consume the modifier and lower to the same
22972 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22973 Token::Ident(s)
22974 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22975 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22976 || w.eq_ignore_ascii_case("without"))
22977 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22978 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22979 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22980 {
22981 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22982 self.advance(); // WITH / WITHOUT
22983 self.advance(); // TIME
22984 self.advance(); // ZONE
22985 let Token::String(lit) = self.advance() else {
22986 unreachable!("guard checked a string token");
22987 };
22988 let base = s.to_ascii_lowercase();
22989 let target = match (base.as_str(), with_tz) {
22990 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22991 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22992 (_, true) => CastTarget::Timestamptz,
22993 (_, false) => CastTarget::Timestamp,
22994 };
22995 Ok(Expr::Cast {
22996 expr: Box::new(Expr::Literal(Literal::String(lit))),
22997 target,
22998 })
22999 }
23000 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23001 // gathers the subquery's single-column rows (in its row order)
23002 // into an array. Desugared to `array_agg` over the subquery as a
23003 // derived table; out-of-line to keep parse_atom's frame small (it
23004 // sits on the recursive nesting-budget cycle).
23005 Token::Ident(s) | Token::QuotedIdent(s)
23006 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23007 {
23008 self.parse_array_subquery()
23009 }
23010 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23011 // is not a reserved token; we match by case-insensitive
23012 // ident. The opening `[` must follow immediately. v7.39 (read01
23013 // round 105) — the body moved out-of-line so its `Vec`/loop locals
23014 // leave parse_atom's frame (which sits on the nesting-budget cycle).
23015 Token::Ident(s) | Token::QuotedIdent(s)
23016 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23017 {
23018 self.parse_array_literal_body()
23019 }
23020 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23021 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23022 // We special-case before the generic ident dispatch so
23023 // the AGAINST clause never reaches the function-call
23024 // loop (which would mis-read `(cols) AGAINST` as a
23025 // call with no trailing modifier). The shape is
23026 // rewritten to a Boolean OR over per-column
23027 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23028 // term)` so the existing FTS evaluator handles
23029 // semantics — the fulltext-GIN built at CREATE TABLE
23030 // time is currently a "real index that survives dump
23031 // round-trip"; the planner hook that actually uses
23032 // it for posting-list intersection lands in a later
23033 // sub-phase (Phase 2.2b) without touching this surface.
23034 Token::Ident(s) | Token::QuotedIdent(s)
23035 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23036 {
23037 self.parse_match_against_atom()
23038 }
23039 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23040 // v7.37.43-T4 — PG-unreserved keywords are legal column /
23041 // alias names in expression context too. `release` appears
23042 // in sentori `0003_partition_events.sql` as both a column
23043 // reference (SELECT … release …) and an INSERT column list
23044 // entry. Mirrors `expect_ident_like`'s expansion of the
23045 // identifier set.
23046 other if unreserved_keyword_text(&other).is_some() => {
23047 let s = unreserved_keyword_text(&other).unwrap();
23048 self.finish_ident_atom(s)
23049 }
23050 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23051 // only inside `SET` before, so `SELECT @@autocommit` — which
23052 // every MySQL connector asks at handshake — was a parse error.
23053 // MariaDB accepts the bare, `@@session.` and `@@global.`
23054 // spellings alike and answers from the session's own value.
23055 Token::SessionVar(v) => {
23056 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23057 // has nothing to do with a `@@` engine setting: its own
23058 // per-session namespace, and an unset one reads NULL instead
23059 // of raising. Stripping every `@` (as this did) made `@x` and
23060 // `@@x` the same node, so `SELECT @x` answered "Unknown
23061 // system variable".
23062 Ok(variable_ref_atom(&v))
23063 }
23064 other => Err(ParseError {
23065 message: format!("unexpected token {other:?} in expression"),
23066 token_pos: tok_pos,
23067 }),
23068 }
23069 // After parsing the atom, fold any postfix `::vector` casts.
23070 .and_then(|atom| self.finish_postfix_casts(atom))
23071 }
23072
23073 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23074 /// Both bind tighter than any binary op.
23075 /// Shared cast-target parser for postfix `::TYPE` and the
23076 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23077 /// If the next tokens are `( N )`, consume them and return the canonical
23078 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23079 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23080 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23081 if !matches!(self.peek(), Token::LParen) {
23082 return None;
23083 }
23084 self.advance(); // (
23085 let n = match self.advance() {
23086 Token::Integer(n) => n,
23087 _ => return Some(base.to_string()), // malformed → drop precision
23088 };
23089 if matches!(self.peek(), Token::RParen) {
23090 self.advance();
23091 }
23092 Some(alloc::format!("{base}({n})"))
23093 }
23094
23095 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23096 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23097 // schema-qualifies every cast target, and `pg_catalog.X` names
23098 // exactly the builtin type X. Consume the qualifier and let
23099 // the ordinary target parse decide.
23100 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23101 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23102 {
23103 self.advance();
23104 self.advance();
23105 }
23106 let target = match self.advance() {
23107 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23108 "int" | "integer" | "int4" => {
23109 if matches!(self.peek(), Token::LBracket)
23110 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23111 {
23112 self.advance();
23113 self.advance();
23114 CastTarget::IntArray
23115 } else {
23116 CastTarget::Int
23117 }
23118 }
23119 "bigint" | "int8" => {
23120 if matches!(self.peek(), Token::LBracket)
23121 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23122 {
23123 self.advance();
23124 self.advance();
23125 CastTarget::BigIntArray
23126 } else {
23127 CastTarget::BigInt
23128 }
23129 }
23130 "float" | "double" => CastTarget::Float,
23131 "text" => {
23132 // v7.10.11 — `::TEXT[]` widens to TextArray.
23133 if matches!(self.peek(), Token::LBracket)
23134 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23135 {
23136 self.advance();
23137 self.advance();
23138 CastTarget::TextArray
23139 } else {
23140 CastTarget::Text
23141 }
23142 }
23143 "bool" | "boolean" => CastTarget::Bool,
23144 "vector" => CastTarget::Vector,
23145 "date" => CastTarget::Date,
23146 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23147 // seconds precision through the Named path (the engine rounds
23148 // the sub-second field); bare `::timestamp` keeps the fast arm.
23149 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23150 Some(named) => CastTarget::Named(named),
23151 None => CastTarget::Timestamp,
23152 },
23153 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23154 Some(named) => CastTarget::Named(named),
23155 None => CastTarget::Timestamptz,
23156 },
23157 "interval" => CastTarget::Interval,
23158 "json" => CastTarget::Json,
23159 "jsonb" => CastTarget::Jsonb,
23160 // v7.39 (round 694) — these have dedicated CastTarget
23161 // variants, so they never reached the postfix `[]` handling
23162 // further down and `::regtype[]` was a SYNTAX error at the
23163 // `]`. PG has an array type for every scalar; take the
23164 // suffix here and hand the canonical `<ty>_array` name to
23165 // the engine, the same shape every other array cast uses.
23166 "regtype" if self.peek_postfix_array_brackets() => {
23167 self.advance();
23168 self.advance();
23169 CastTarget::Named(alloc::string::String::from("regtype_array"))
23170 }
23171 "regclass" if self.peek_postfix_array_brackets() => {
23172 self.advance();
23173 self.advance();
23174 CastTarget::Named(alloc::string::String::from("regclass_array"))
23175 }
23176 "regtype" => CastTarget::RegType,
23177 "regclass" => CastTarget::RegClass,
23178 // v7.12.0 — `::tsvector` / `::tsquery`.
23179 // Engine decodes the LHS text via the PG
23180 // external form parser.
23181 // v7.39 (round 352, M8) — MySQL's own cast targets.
23182 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23183 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23184 // such type, so they are taken only in that dialect and
23185 // fall through to the "type does not exist" arm otherwise.
23186 "signed" | "unsigned" if self.mysql_dialect => {
23187 if matches!(self.peek(), Token::Ident(k)
23188 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23189 {
23190 self.advance();
23191 }
23192 CastTarget::Named(s.to_ascii_lowercase())
23193 }
23194 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23195 // in MySQL: MariaDB answers '123' where the SQL-standard
23196 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23197 // Truncating a number to its first digit is a wrong answer
23198 // with no error, so the MySQL session gets MySQL's reading.
23199 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23200 CastTarget::Text
23201 }
23202 "tsvector" => CastTarget::TsVector,
23203 "tsquery" => CastTarget::TsQuery,
23204 // v7.17.0 — `::uuid`. Engine decodes the LHS
23205 // text via `spg_storage::parse_uuid_str`.
23206 "uuid" => CastTarget::Uuid,
23207 // v7.18 — `::bytea`. Engine decodes the LHS
23208 // text via the PG hex form (`'\xdeadbeef'`)
23209 // or escape form (`'\\x05\\x00'`). Closes
23210 // mailrs D-pre #3 reverse-acceptance gap.
23211 "bytea" => CastTarget::Bytea,
23212 // v7.37.5 ship triage — generic typed-cast escape.
23213 // Anything the long-tail PG type ident table knows
23214 // about(network/bit/geometry/multirange/etc.)flows
23215 // through `CastTarget::Named(canonical)`; the engine
23216 // resolves via `column_type_to_data_type` and dispatches
23217 // through the typed `coerce_value` path. Truly
23218 // unrecognised idents still hit the error arm below
23219 // because the engine rejects them.
23220 other => {
23221 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23222 // `::varchar(255)`, etc. Capture into the canonical
23223 // `name(p,s)` form so `type_name_to_data_type` can
23224 // reconstruct the `DataType::Numeric { precision,
23225 // scale }` (and similar param-carrying types).
23226 let mut name = other.to_string();
23227 // v7.39 (round 281) — `::bit varying(3)` is two
23228 // words; fold the tail in so the typmod reaches the
23229 // type resolver instead of tripping the parser.
23230 if name.eq_ignore_ascii_case("bit")
23231 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23232 {
23233 self.advance();
23234 name = alloc::string::String::from("varbit");
23235 }
23236 // v7.39 (round 613) — `::character varying` is the same
23237 // two-word shape and had no fold, so the `varying` was
23238 // left behind and the cast became a bare `character`,
23239 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23240 // `a` where PG answers `ab`. Silently, and for a spelling
23241 // pg_dump writes.
23242 if name.eq_ignore_ascii_case("character")
23243 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23244 {
23245 self.advance();
23246 name = alloc::string::String::from("varchar");
23247 }
23248 if matches!(self.peek(), Token::LParen) {
23249 let mut buf = alloc::string::String::from("(");
23250 let mut depth = 0usize;
23251 loop {
23252 match self.advance() {
23253 Token::LParen => {
23254 depth += 1;
23255 if depth > 1 {
23256 buf.push('(');
23257 }
23258 }
23259 Token::RParen => {
23260 depth -= 1;
23261 if depth == 0 {
23262 buf.push(')');
23263 break;
23264 }
23265 buf.push(')');
23266 }
23267 Token::Comma => buf.push(','),
23268 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23269 // v7.39 (round 273) — a minus used to fall
23270 // into the catch-all below and vanish, so
23271 // `::numeric(10,-2)` reached the engine as
23272 // the text `numeric(10,2)` and silently
23273 // rounded to two DECIMALS instead of to
23274 // hundreds. A dropped token is not a
23275 // no-op when it carries a sign.
23276 Token::Minus => buf.push('-'),
23277 Token::Eof => break,
23278 _ => {}
23279 }
23280 }
23281 name.push_str(&buf);
23282 }
23283 // Optional postfix `[]` widens to the array form —
23284 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23285 // The engine's `type_name_to_data_type` recognises
23286 // the canonical `<ty>_array` form.
23287 if matches!(self.peek(), Token::LBracket)
23288 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23289 {
23290 self.advance();
23291 self.advance();
23292 name.push_str("_array");
23293 }
23294 CastTarget::Named(name)
23295 }
23296 },
23297 Token::Interval => CastTarget::Interval,
23298 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23299 // "char" (oid 18, SPG Char1 — distinct from bare `char`
23300 // = char(1)); other quoted names resolve like idents.
23301 Token::QuotedIdent(q) => {
23302 if q.eq_ignore_ascii_case("char") {
23303 CastTarget::Named("char1".into())
23304 } else {
23305 CastTarget::Named(q.to_ascii_lowercase())
23306 }
23307 }
23308 other => {
23309 return Err(ParseError {
23310 message: format!("expected type ident after `::`, got {other:?}"),
23311 token_pos: self.consumed_pos(),
23312 });
23313 }
23314 };
23315 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23316 // target to its array sibling. Closed-enum arms (Bool /
23317 // SmallInt / Numeric / Float / Date / …) didn't carry the
23318 // explicit widening that Text / Int / BigInt did, so
23319 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23320 // error. The widening here mirrors the per-arm Text /
23321 // Int / BigInt logic above + folds the new ζ-A first-class
23322 // types through `CastTarget::Named("<ty>_array")`.
23323 if matches!(self.peek(), Token::LBracket)
23324 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23325 {
23326 let widened = match &target {
23327 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23328 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23329 // v7.39 (round 326, V43) — the two temporal types stay
23330 // distinct. Both used to widen to `timestamptz_array`, so
23331 // `::timestamp[]` named the wrong target in its own error
23332 // message and lost the zone-less identity on the way.
23333 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23334 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23335 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23336 CastTarget::Json | CastTarget::Jsonb => {
23337 Some(CastTarget::Named("jsonb_array".to_string()))
23338 }
23339 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23340 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23341 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23342 CastTarget::Named(name) => {
23343 let mut a = name.clone();
23344 a.push_str("_array");
23345 Some(CastTarget::Named(a))
23346 }
23347 // Int / BigInt / Text / Vector / TsVector / TsQuery /
23348 // RegType / RegClass / TextArray / IntArray /
23349 // BigIntArray already finalised — leave as is.
23350 _ => None,
23351 };
23352 if let Some(w) = widened {
23353 self.advance();
23354 self.advance();
23355 return Ok(w);
23356 }
23357 }
23358 Ok(target)
23359 }
23360
23361 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23362 loop {
23363 // v7.38 (read01, T9) — composite field access `(expr).field`.
23364 // A bare `a.b` is consumed as a qualified column inside the ident
23365 // atom, so a Dot only survives to this postfix position when the
23366 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23367 // `.*` whole-row expansion is not handled here (projection-level).
23368 if matches!(self.peek(), Token::Dot)
23369 && matches!(
23370 self.tokens.get(self.pos + 1),
23371 Some(Token::Ident(_) | Token::QuotedIdent(_))
23372 )
23373 {
23374 self.advance(); // .
23375 let field = match self.advance() {
23376 Token::Ident(s) | Token::QuotedIdent(s) => s,
23377 other => {
23378 return Err(
23379 self.err(format!("expected a field name after '.', got {other:?}"))
23380 );
23381 }
23382 };
23383 expr = Expr::FieldAccess {
23384 base: Box::new(expr),
23385 field,
23386 };
23387 continue;
23388 }
23389 if matches!(self.peek(), Token::DoubleColon) {
23390 self.advance();
23391 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23392 // target set to include INTERVAL (reserved Token),
23393 // TIMESTAMPTZ, and PG catalog regtype / regclass.
23394 // mailrs follow-up H3a + H3b.
23395 let target = self.parse_cast_target()?;
23396 expr = Expr::Cast {
23397 expr: Box::new(expr),
23398 target,
23399 };
23400 continue;
23401 }
23402 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23403 // returns NULL for out-of-range. Multiple subscripts
23404 // chain: `a[i][j]` parses left-to-right.
23405 if matches!(self.peek(), Token::LBracket) {
23406 self.advance();
23407 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23408 // bare index stays a subscript.
23409 let lo = if matches!(self.peek(), Token::Colon) {
23410 None
23411 } else {
23412 Some(self.parse_expr(0)?)
23413 };
23414 if matches!(self.peek(), Token::Colon) {
23415 self.advance();
23416 let hi = if matches!(self.peek(), Token::RBracket) {
23417 None
23418 } else {
23419 Some(Box::new(self.parse_expr(0)?))
23420 };
23421 if !matches!(self.peek(), Token::RBracket) {
23422 return Err(self.err(alloc::format!(
23423 "expected ']' after array slice, got {:?}",
23424 self.peek()
23425 )));
23426 }
23427 self.advance();
23428 expr = Expr::ArraySlice {
23429 target: Box::new(expr),
23430 lo: lo.map(Box::new),
23431 hi,
23432 };
23433 continue;
23434 }
23435 let index = lo.expect("non-colon branch parsed an index");
23436 if !matches!(self.peek(), Token::RBracket) {
23437 return Err(self.err(alloc::format!(
23438 "expected ']' after array index, got {:?}",
23439 self.peek()
23440 )));
23441 }
23442 self.advance();
23443 expr = Expr::ArraySubscript {
23444 target: Box::new(expr),
23445 index: Box::new(index),
23446 };
23447 continue;
23448 }
23449 // `expr AT TIME ZONE zone` — lowers to PG's own function
23450 // form timezone(zone, expr); the scalar implements the
23451 // offset shift (named zones error there — no tzdata).
23452 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23453 && matches!(self.tokens.get(self.pos + 1),
23454 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23455 && matches!(self.tokens.get(self.pos + 2),
23456 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23457 {
23458 self.advance(); // AT
23459 self.advance(); // TIME
23460 self.advance(); // ZONE
23461 // Zone at comparison precedence so AND/OR stay out.
23462 let zone = self.parse_expr(6)?;
23463 expr = Expr::FunctionCall {
23464 name: "timezone".to_string(),
23465 args: alloc::vec![zone, expr],
23466 };
23467 continue;
23468 }
23469 // `expr COLLATE "name"` — SPG's single text ordering IS
23470 // byte order, i.e. the C collation. The byte-order
23471 // spellings absorb as no-ops; a locale collation would
23472 // silently sort differently from PG, so it errors
23473 // honestly instead.
23474 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23475 self.advance();
23476 let mut cname = match self.advance() {
23477 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23478 other => {
23479 return Err(self.err(alloc::format!(
23480 "expected collation name after COLLATE, got {other:?}"
23481 )));
23482 }
23483 };
23484 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23485 // is how `pg_dump` writes the default one:
23486 // `… COLLATE pg_catalog.default`. Reading a single token
23487 // left the SCHEMA as the name, so the clause was refused
23488 // as an unsupported locale collation and no dump ran.
23489 if matches!(self.peek(), Token::Dot) {
23490 // v7.39.2 — the qualifier is DROPPED (SPG is single
23491 // schema) but it is checked first. PostgreSQL 18.6
23492 // answers `schema "nosuch_schema" does not exist` for
23493 // one it has never heard of, and dropping it unread
23494 // meant `COLLATE nosuch_schema."C"` succeeded here —
23495 // a name that names nothing, accepted.
23496 let schema = cname.to_ascii_lowercase();
23497 if !matches!(
23498 schema.as_str(),
23499 "pg_catalog" | "public" | "information_schema"
23500 ) {
23501 return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23502 }
23503 self.advance();
23504 cname = match self.advance() {
23505 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23506 // `default` lexes as a KEYWORD, and it is the name
23507 // pg_dump writes — the same trap round 535 hit with
23508 // TABLE / INDEX / FULL.
23509 Token::Default => alloc::string::String::from("default"),
23510 other => {
23511 return Err(self.err(alloc::format!(
23512 "expected collation name after COLLATE, got {other:?}"
23513 )));
23514 }
23515 };
23516 }
23517 let lc = cname.to_ascii_lowercase();
23518 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23519 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23520 // family / `binary`) forces byte-wise, which is exactly
23521 // what `BINARY expr` does — lower onto that so every fold
23522 // site (comparison, LIKE, ORDER BY) suppresses via
23523 // `is_binary_coerced`. A `_ci` family override folds, and
23524 // under the MySQL dialect the default already folds, so it
23525 // absorbs as a no-op; likewise the C / byte-order spellings.
23526 // v7.39.2 — against MySQL's own list, not against the
23527 // shape of the name. `nosuch_bin` took this shortcut and
23528 // became a BINARY cast; `nosuch_ci` took the one below
23529 // and was absorbed as a no-op. Either way the client
23530 // named a collation that does not exist and was told
23531 // nothing. An unknown name now falls through to the
23532 // node, and the engine refuses it.
23533 let real = crate::charset::is_mysql_collation(&lc);
23534 // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23535 //
23536 // They are not the same thing, and folding them together
23537 // lost a bit. Measured on MySQL 9.7.2 with the connection
23538 // on utf8mb4:
23539 //
23540 // ```text
23541 // 'a ' = 'a' COLLATE utf8mb4_bin 1 PAD SPACE
23542 // 'a ' = 'a' COLLATE utf8mb4_0900_bin 0 NO PAD
23543 // 'AB' = 'ab' COLLATE utf8mb4_bin 0 byte-wise
23544 // ```
23545 //
23546 // The BINARY cast carries "byte-wise" and, with it,
23547 // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23548 // the first line. Keeping the node lets `text_compare_of`
23549 // read the NAME and settle the two bits separately: it
23550 // does not fold (`folds_case` says so) and it does pad
23551 // (`pads_space` says so), while `is_byte_wise` still
23552 // keeps the ORDERING off the locale.
23553 if self.mysql_dialect && real && lc == "binary" {
23554 expr = Expr::Cast {
23555 expr: alloc::boxed::Box::new(expr),
23556 target: CastTarget::Named("binary".to_string()),
23557 };
23558 continue;
23559 }
23560 let mysql_ci = self.mysql_dialect
23561 && ((real && lc.ends_with("_ci"))
23562 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23563 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23564 // goes to the lowering channel, the byte-order spellings
23565 // included. Round 691 recorded only the names the old
23566 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23567 // absorbed as a no-op — and once a column could declare a
23568 // collation, absorbing the clause meant the COLUMN's
23569 // collation won where the query had asked for bytes.
23570 if self.in_order_by_key && !mysql_ci {
23571 self.order_key_collation = Some(cname);
23572 continue;
23573 }
23574 // v7.39.2 — the clause becomes a NODE rather than being
23575 // refused or absorbed.
23576 //
23577 // What stood here refused the locale names and SILENTLY
23578 // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23579 // answered `t` where PostgreSQL 18.6 answers `f`: the one
23580 // family it let through is the one where dropping it
23581 // changes the answer. Absorbing is only correct when the
23582 // collation asked for is the one the comparison would use
23583 // anyway, and that depends on the DATABASE — which the
23584 // parser cannot see. So it rides along and the engine,
23585 // which can, decides.
23586 //
23587 // `collate_derive` already modelled `Explicit(name)` and
23588 // had no way to be handed one.
23589 // v7.39.2 — a MySQL spelling does not exist on the
23590 // PostgreSQL wire, and THIS is where the wire is known.
23591 //
23592 // The check lived in the evaluator first and asked
23593 // `ctx.mysql_dialect`, which the INSERT path builds as a
23594 // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23595 // in a MySQL session was refused for a collation that
23596 // does not exist on a wire it was not on. Making that
23597 // context truthful would change INSERT-time evaluation
23598 // in other ways as a side effect; the parser already
23599 // gates the introducer on the same flag and is the
23600 // honest place to ask.
23601 if !self.mysql_dialect
23602 && (lc.ends_with("_ci")
23603 || lc.ends_with("_cs")
23604 || lc.ends_with("_bin")
23605 || lc == "binary"
23606 || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23607 {
23608 return Err(self.err(alloc::format!(
23609 "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23610 )));
23611 }
23612 // v7.39.3 — the node is built for EVERY name, `_ci`
23613 // included.
23614 //
23615 // A MySQL `_ci` spelling used to be absorbed here on the
23616 // reasoning that a MySQL session folds anyway, so the
23617 // clause asked for what it would have got. That stopped
23618 // being true when the fold learned to read the session's
23619 // collation NAME: under `SET NAMES utf8mb4 COLLATE
23620 // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23621 // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23622 // that would have made it 1 had been dropped in the
23623 // parser. Absorbing is only ever correct when the
23624 // collation asked for is the one the comparison would use
23625 // anyway, and the parser cannot know that — the same
23626 // reasoning already written above for the byte-order
23627 // spellings, applied to the family it had exempted.
23628 expr = Expr::Collate {
23629 expr: alloc::boxed::Box::new(expr),
23630 collation: cname,
23631 };
23632 continue;
23633 }
23634 return Ok(expr);
23635 }
23636 }
23637
23638 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23639 /// the first token that is not one. Schema qualifiers collapse to the
23640 /// last part, which is what every other name path here does (SPG is
23641 /// single-schema).
23642 fn take_comma_separated_names(&mut self) -> Vec<String> {
23643 let mut out = Vec::new();
23644 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23645 self.advance();
23646 let mut last = n;
23647 while matches!(self.peek(), Token::Dot) {
23648 self.advance();
23649 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23650 last = t;
23651 }
23652 }
23653 out.push(last);
23654 if matches!(self.peek(), Token::Comma) {
23655 self.advance();
23656 } else {
23657 break;
23658 }
23659 }
23660 out
23661 }
23662
23663 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23664 ///
23665 /// The general cast-target path tests this inline; the types with their
23666 /// own `CastTarget` variant need it as a guard on their match arm,
23667 /// which is what this exists for.
23668 fn peek_postfix_array_brackets(&self) -> bool {
23669 matches!(self.peek(), Token::LBracket)
23670 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23671 }
23672
23673 /// Parse the operator tail after a `(a, b, …)` row constructor
23674 /// and expand at parse time. `=` is the conjunction of element
23675 /// equalities; `<>` its negation; the order operators expand
23676 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23677 /// equalities. Anything else (a bare row value, a subquery
23678 /// RHS) errors honestly — SPG has no composite runtime value.
23679 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23680 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23681 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23682 lhs: Box::new(l.clone()),
23683 op: BinOp::Eq,
23684 rhs: Box::new(r.clone()),
23685 });
23686 let first = it.next().expect("row has at least two elements");
23687 it.fold(first, |acc, e| Expr::Binary {
23688 lhs: Box::new(acc),
23689 op: BinOp::And,
23690 rhs: Box::new(e),
23691 })
23692 }
23693 // Lexicographic (a,b) OP (c,d):
23694 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
23695 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23696 if lhs.len() == 1 {
23697 return Expr::Binary {
23698 lhs: Box::new(lhs[0].clone()),
23699 op: last,
23700 rhs: Box::new(rhs[0].clone()),
23701 };
23702 }
23703 let head_strict = Expr::Binary {
23704 lhs: Box::new(lhs[0].clone()),
23705 op: strict,
23706 rhs: Box::new(rhs[0].clone()),
23707 };
23708 let head_eq = Expr::Binary {
23709 lhs: Box::new(lhs[0].clone()),
23710 op: BinOp::Eq,
23711 rhs: Box::new(rhs[0].clone()),
23712 };
23713 Expr::Binary {
23714 lhs: Box::new(head_strict),
23715 op: BinOp::Or,
23716 rhs: Box::new(Expr::Binary {
23717 lhs: Box::new(head_eq),
23718 op: BinOp::And,
23719 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23720 }),
23721 }
23722 }
23723 let negated_in = if matches!(self.peek(), Token::Not)
23724 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23725 {
23726 self.advance();
23727 true
23728 } else {
23729 false
23730 };
23731 if matches!(self.peek(), Token::In) {
23732 self.advance();
23733 if !matches!(self.peek(), Token::LParen) {
23734 return Err(self.err(alloc::format!(
23735 "expected '(' after row IN, got {:?}",
23736 self.peek()
23737 )));
23738 }
23739 self.advance();
23740 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23741 // not a list of literal rows. Row-vs-list decomposes to
23742 // OR-of-AND above, but the subquery's rows are only known at
23743 // runtime, so keep it as a RowInSubquery node.
23744 if matches!(self.peek(), Token::Select) {
23745 let inner = self.parse_select_stmt()?;
23746 if !matches!(self.peek(), Token::RParen) {
23747 return Err(self.err(alloc::format!(
23748 "expected ')' after row IN-subquery, got {:?}",
23749 self.peek()
23750 )));
23751 }
23752 self.advance();
23753 let Statement::Select(s) = inner else {
23754 unreachable!("parse_select_stmt always returns Statement::Select")
23755 };
23756 return Ok(Expr::RowInSubquery {
23757 row,
23758 subquery: Box::new(s),
23759 negated: negated_in,
23760 });
23761 }
23762 let mut alternatives: Vec<Expr> = Vec::new();
23763 loop {
23764 // Optional ROW keyword before the paren row.
23765 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23766 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23767 {
23768 self.advance();
23769 }
23770 if !matches!(self.peek(), Token::LParen) {
23771 return Err(self.err(alloc::format!(
23772 "expected '(' to open a row inside IN, got {:?}",
23773 self.peek()
23774 )));
23775 }
23776 self.advance();
23777 let mut rhs = alloc::vec![self.parse_expr(0)?];
23778 while matches!(self.peek(), Token::Comma) {
23779 self.advance();
23780 rhs.push(self.parse_expr(0)?);
23781 }
23782 if !matches!(self.peek(), Token::RParen) {
23783 return Err(self.err(alloc::format!(
23784 "expected ')' after row inside IN, got {:?}",
23785 self.peek()
23786 )));
23787 }
23788 self.advance();
23789 if rhs.len() != row.len() {
23790 return Err(self.err(alloc::format!(
23791 "row IN arity mismatch: left has {}, right has {}",
23792 row.len(),
23793 rhs.len()
23794 )));
23795 }
23796 alternatives.push(row_eq(&row, &rhs));
23797 if matches!(self.peek(), Token::Comma) {
23798 self.advance();
23799 continue;
23800 }
23801 break;
23802 }
23803 if !matches!(self.peek(), Token::RParen) {
23804 return Err(self.err(alloc::format!(
23805 "expected ')' to close row IN list, got {:?}",
23806 self.peek()
23807 )));
23808 }
23809 self.advance();
23810 let mut it = alternatives.into_iter();
23811 let first = it.next().expect("IN list has at least one row");
23812 let combined = it.fold(first, |acc, e| Expr::Binary {
23813 lhs: Box::new(acc),
23814 op: BinOp::Or,
23815 rhs: Box::new(e),
23816 });
23817 return Ok(maybe_not(combined, negated_in));
23818 }
23819 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23820 // two periods share at least one time point. Each pair is
23821 // normalised with least/greatest (PG accepts the endpoints
23822 // in either order), then lowered to the standard
23823 // `start1 < end2 AND start2 < end1` form.
23824 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23825 if row.len() != 2 {
23826 return Err(self.err(alloc::format!(
23827 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23828 row.len()
23829 )));
23830 }
23831 self.advance();
23832 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23833 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23834 {
23835 self.advance();
23836 }
23837 if !matches!(self.peek(), Token::LParen) {
23838 return Err(self.err(alloc::format!(
23839 "expected '(' after OVERLAPS, got {:?}",
23840 self.peek()
23841 )));
23842 }
23843 self.advance();
23844 let r0 = self.parse_expr(0)?;
23845 if !matches!(self.peek(), Token::Comma) {
23846 return Err(self.err(alloc::format!(
23847 "OVERLAPS needs (start, end) on the right, got {:?}",
23848 self.peek()
23849 )));
23850 }
23851 self.advance();
23852 let r1 = self.parse_expr(0)?;
23853 if !matches!(self.peek(), Token::RParen) {
23854 return Err(self.err(alloc::format!(
23855 "expected ')' after OVERLAPS pair, got {:?}",
23856 self.peek()
23857 )));
23858 }
23859 self.advance();
23860 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23861 name: String::from(name),
23862 args: alloc::vec![a.clone(), b.clone()],
23863 };
23864 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23865 lhs: Box::new(lhs),
23866 op: BinOp::Lt,
23867 rhs: Box::new(rhs),
23868 };
23869 return Ok(Expr::Binary {
23870 lhs: Box::new(lt(
23871 pair_fn("least", &row[0], &row[1]),
23872 pair_fn("greatest", &r0, &r1),
23873 )),
23874 op: BinOp::And,
23875 rhs: Box::new(lt(
23876 pair_fn("least", &r0, &r1),
23877 pair_fn("greatest", &row[0], &row[1]),
23878 )),
23879 });
23880 }
23881 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23882 // PG, `IS NULL` is true only when EVERY field is NULL, and
23883 // `IS NOT NULL` is true only when every field is non-NULL — the
23884 // latter is NOT the negation of the former (a mixed row is
23885 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23886 // which reproduces exactly that all-fields semantics.
23887 if matches!(self.peek(), Token::Is) {
23888 self.advance();
23889 let negated = if matches!(self.peek(), Token::Not) {
23890 self.advance();
23891 true
23892 } else {
23893 false
23894 };
23895 if !matches!(self.peek(), Token::Null) {
23896 return Err(self.err(alloc::format!(
23897 "expected NULL after row IS [NOT], got {:?}",
23898 self.peek()
23899 )));
23900 }
23901 self.advance();
23902 let mut it = row.iter().map(|e| Expr::IsNull {
23903 expr: Box::new(e.clone()),
23904 negated,
23905 });
23906 let first = it.next().expect("row has at least two elements");
23907 return Ok(it.fold(first, |acc, e| Expr::Binary {
23908 lhs: Box::new(acc),
23909 op: BinOp::And,
23910 rhs: Box::new(e),
23911 }));
23912 }
23913 let op = match self.peek() {
23914 Token::Eq => BinOp::Eq,
23915 Token::NotEq => BinOp::NotEq,
23916 Token::Lt => BinOp::Lt,
23917 Token::LtEq => BinOp::LtEq,
23918 Token::Gt => BinOp::Gt,
23919 Token::GtEq => BinOp::GtEq,
23920 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23921 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23922 // constructor value, identical to the `ROW(a, b, …)` keyword form:
23923 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23924 // (`::text`, `.field`) applies at the caller just as it does for the
23925 // ROW(...) node. All the comparison / predicate forms returned above.
23926 _ => {
23927 return Ok(Expr::FunctionCall {
23928 name: String::from("row"),
23929 args: row,
23930 });
23931 }
23932 };
23933 self.advance();
23934 // Optional ROW keyword before the paren row.
23935 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23936 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23937 {
23938 self.advance();
23939 }
23940 if !matches!(self.peek(), Token::LParen) {
23941 return Err(self.err(alloc::format!(
23942 "expected '(' to open the right-hand row, got {:?}",
23943 self.peek()
23944 )));
23945 }
23946 self.advance();
23947 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23948 // subquery. Kept as a node (the subquery's row is a runtime value);
23949 // the literal-RHS form below still decomposes at parse time.
23950 if matches!(self.peek(), Token::Select) {
23951 let inner = self.parse_select_stmt()?;
23952 if !matches!(self.peek(), Token::RParen) {
23953 return Err(self.err(alloc::format!(
23954 "expected ')' after row comparison subquery, got {:?}",
23955 self.peek()
23956 )));
23957 }
23958 self.advance();
23959 let Statement::Select(s) = inner else {
23960 unreachable!("parse_select_stmt always returns Statement::Select")
23961 };
23962 return Ok(Expr::RowCmpSubquery {
23963 row,
23964 op,
23965 subquery: Box::new(s),
23966 });
23967 }
23968 let mut rhs = alloc::vec![self.parse_expr(0)?];
23969 while matches!(self.peek(), Token::Comma) {
23970 self.advance();
23971 rhs.push(self.parse_expr(0)?);
23972 }
23973 if !matches!(self.peek(), Token::RParen) {
23974 return Err(self.err(alloc::format!(
23975 "expected ')' after right-hand row, got {:?}",
23976 self.peek()
23977 )));
23978 }
23979 self.advance();
23980 if rhs.len() != row.len() {
23981 // v7.39 (round 239) — PG's wording (42601).
23982 return Err(self.err("unequal number of entries in row expressions".to_string()));
23983 }
23984 Ok(match op {
23985 BinOp::Eq => row_eq(&row, &rhs),
23986 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23987 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23988 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23989 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23990 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23991 _ => unreachable!("op restricted above"),
23992 })
23993 }
23994
23995 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23996 /// escape character becomes the matcher's default backslash:
23997 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23998 /// → the char itself, and any pre-existing backslash escapes
23999 /// itself so it stays literal. Both operands must be string
24000 /// literals — a runtime pattern would need matcher support.
24001 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24002 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24003 (&pattern, &esc)
24004 else {
24005 return Err(
24006 "LIKE ... ESCAPE requires string-literal pattern and escape \
24007 (runtime escape characters are not supported yet)"
24008 .into(),
24009 );
24010 };
24011 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24012 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24013 // multi-character escape is an error.
24014 let esc_ch: Option<char> = {
24015 let mut ch_iter = e.chars();
24016 match (ch_iter.next(), ch_iter.next()) {
24017 (Some(c), None) => Some(c),
24018 (None, _) => None,
24019 (Some(_), Some(_)) => {
24020 return Err(alloc::format!(
24021 "ESCAPE must be a single character, got {e:?}"
24022 ));
24023 }
24024 }
24025 };
24026 let mut out = String::with_capacity(p.len() + 4);
24027 let mut chars = p.chars();
24028 while let Some(c) = chars.next() {
24029 if Some(c) == esc_ch {
24030 match chars.next() {
24031 // Escaped wildcard / escaped escape → keep the
24032 // next char literal via backslash.
24033 Some(next) => {
24034 out.push('\\');
24035 out.push(next);
24036 }
24037 None => {
24038 return Err("LIKE pattern ends with the escape character".into());
24039 }
24040 }
24041 } else if c == '\\' && esc_ch != Some('\\') {
24042 // A raw backslash is literal under a custom (or absent) escape
24043 // — escape it for the backslash-based matcher.
24044 out.push('\\');
24045 out.push('\\');
24046 } else {
24047 out.push(c);
24048 }
24049 }
24050 Ok(Expr::Literal(Literal::String(out)))
24051 }
24052
24053 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24054 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24055 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24056 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24057 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24058 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24059 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24060 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24061 /// array expression errors honestly rather than silently mismatching.
24062 fn try_like_any_all(
24063 &mut self,
24064 base: &Expr,
24065 negated: bool,
24066 case_insensitive: bool,
24067 ) -> Result<Option<Expr>, ParseError> {
24068 let is_any = match self.peek() {
24069 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24070 Token::Ident(s)
24071 if s.eq_ignore_ascii_case("any")
24072 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24073 {
24074 true
24075 }
24076 _ => return Ok(None),
24077 };
24078 self.advance(); // ANY / ALL
24079 self.advance(); // '('
24080 let arr = self.parse_expr(0)?;
24081 if !matches!(self.peek(), Token::RParen) {
24082 return Err(self.err(format!(
24083 "expected ')' after LIKE {} argument, got {:?}",
24084 if is_any { "ANY" } else { "ALL" },
24085 self.peek()
24086 )));
24087 }
24088 self.advance(); // ')'
24089 let Expr::Array(items) = arr else {
24090 return Err(self.err(
24091 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24092 ));
24093 };
24094 let mut clauses = items.into_iter().map(|p| Expr::Like {
24095 expr: Box::new(base.clone()),
24096 pattern: Box::new(p),
24097 negated,
24098 case_insensitive,
24099 });
24100 let Some(first) = clauses.next() else {
24101 // ANY(empty) = FALSE, ALL(empty) = TRUE.
24102 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24103 };
24104 let op = if is_any { BinOp::Or } else { BinOp::And };
24105 let combined = clauses.fold(first, |acc, c| Expr::Binary {
24106 lhs: Box::new(acc),
24107 op,
24108 rhs: Box::new(c),
24109 });
24110 Ok(Some(combined))
24111 }
24112
24113 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
24114 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24115 /// `AND` is not swallowed.
24116 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24117 self.advance(); // BETWEEN
24118 // SYMMETRIC — the bounds may arrive in either order; both
24119 // orientations OR together. ASYMMETRIC is the default and
24120 // absorbs as noise.
24121 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24122 {
24123 self.advance();
24124 true
24125 } else {
24126 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24127 self.advance();
24128 }
24129 false
24130 };
24131 let low = self.parse_expr(6)?;
24132 if !matches!(self.peek(), Token::And) {
24133 return Err(self.err(format!(
24134 "expected AND after BETWEEN low bound, got {:?}",
24135 self.peek()
24136 )));
24137 }
24138 self.advance();
24139 let high = self.parse_expr(6)?;
24140 let target = Box::new(expr);
24141 let range = |lo: Expr, hi: Expr| Expr::Binary {
24142 lhs: Box::new(Expr::Binary {
24143 lhs: target.clone(),
24144 op: BinOp::GtEq,
24145 rhs: Box::new(lo),
24146 }),
24147 op: BinOp::And,
24148 rhs: Box::new(Expr::Binary {
24149 lhs: target.clone(),
24150 op: BinOp::LtEq,
24151 rhs: Box::new(hi),
24152 }),
24153 };
24154 let combined = if symmetric {
24155 Expr::Binary {
24156 lhs: Box::new(range(low.clone(), high.clone())),
24157 op: BinOp::Or,
24158 rhs: Box::new(range(high, low)),
24159 }
24160 } else {
24161 range(low, high)
24162 };
24163 Ok(maybe_not(combined, negated))
24164 }
24165
24166 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
24167 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24168 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24169 /// Caller already consumed the leading `WITH` ident.
24170 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24171 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24172 /// self-reference that appears more than once in a single term.
24173 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24174 use crate::ast::{CteBody, SelectStatement};
24175 if !cte.recursive {
24176 return Ok(());
24177 }
24178 let CteBody::Select(body) = &cte.body else {
24179 return Ok(());
24180 };
24181 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24182 // check the anchor and every peer term.
24183 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24184 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24185 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24186 return Err(self.err(String::from(
24187 "ORDER BY in a recursive query is not implemented",
24188 )));
24189 }
24190 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24191 return Err(self.err(String::from(
24192 "LIMIT in a recursive query is not implemented",
24193 )));
24194 }
24195 let self_refs = |s: &SelectStatement| -> usize {
24196 let Some(from) = &s.from else {
24197 return 0;
24198 };
24199 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24200 for j in &from.joins {
24201 if j.table.name.eq_ignore_ascii_case(&cte.name) {
24202 n += 1;
24203 }
24204 }
24205 n
24206 };
24207 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24208 return Err(self.err(alloc::format!(
24209 "recursive reference to query \"{}\" must not appear more than once",
24210 cte.name
24211 )));
24212 }
24213 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24214 // apply only when the body actually references itself (a non-self-
24215 // referencing CTE under WITH RECURSIVE may use any set-op shape).
24216 let anchor_refs = self_refs(body);
24217 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24218 if anchor_refs > 0 || union_refs {
24219 // Shape: the top level must be UNION [ALL] arms only. A self-ref
24220 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24221 // "does not have the form" error — SPG used to compute a value.
24222 if body.unions.is_empty()
24223 || body.unions.iter().any(|(k, _)| {
24224 !matches!(
24225 k,
24226 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24227 )
24228 })
24229 {
24230 return Err(self.err(alloc::format!(
24231 "recursive query \"{}\" does not have the form non-recursive-term \
24232 UNION [ALL] recursive-term",
24233 cte.name
24234 )));
24235 }
24236 if anchor_refs > 0 {
24237 return Err(self.err(alloc::format!(
24238 "recursive reference to query \"{}\" must not appear within its non-recursive term",
24239 cte.name
24240 )));
24241 }
24242 }
24243 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24244 for (_, u) in &body.unions {
24245 if self_refs(u) == 0 {
24246 continue;
24247 }
24248 // The self-reference must not sit on the nullable side of an outer
24249 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24250 if let Some(from) = &u.from {
24251 for (i, j) in from.joins.iter().enumerate() {
24252 let left_has_self = is_self(&from.primary)
24253 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24254 let violated = match j.kind {
24255 crate::ast::JoinKind::Left => is_self(&j.table),
24256 crate::ast::JoinKind::Right => left_has_self,
24257 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24258 _ => false,
24259 };
24260 if violated {
24261 return Err(self.err(alloc::format!(
24262 "recursive reference to query \"{}\" must not appear within an outer join",
24263 cte.name
24264 )));
24265 }
24266 }
24267 }
24268 // No aggregates at the top level of the recursive term (SPG used
24269 // to run them and surface a misleading downstream error).
24270 let mut items_and_having: Vec<&Expr> = Vec::new();
24271 for it in &u.items {
24272 if let crate::ast::SelectItem::Expr { expr, .. } = it {
24273 items_and_having.push(expr);
24274 }
24275 }
24276 if let Some(h) = &u.having {
24277 items_and_having.push(h);
24278 }
24279 for e in items_and_having {
24280 if expr_has_toplevel_aggregate(e) {
24281 return Err(self.err(String::from(
24282 "aggregate functions are not allowed in a recursive query's recursive term",
24283 )));
24284 }
24285 }
24286 }
24287 // A self-reference inside a sublink expression (EXISTS / IN / scalar
24288 // subquery) anywhere in the body is rejected; a plain FROM derived
24289 // table is legal in PG and untouched here.
24290 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24291 all_terms.extend(body.unions.iter().map(|(_, u)| u));
24292 for term in all_terms {
24293 if select_has_self_ref_in_sublink(term, &cte.name) {
24294 return Err(self.err(alloc::format!(
24295 "recursive reference to query \"{}\" must not appear within a subquery",
24296 cte.name
24297 )));
24298 }
24299 }
24300 Ok(())
24301 }
24302
24303 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24304 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24305 /// right after parse so the engine sees a plain recursive CTE with the
24306 /// tracking columns already projected. DEPTH FIRST and CYCLE are
24307 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24308 /// text-rendered rows can't provide, and errors honestly.
24309 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24310 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24311 if cte.search.is_none() && cte.cycle.is_none() {
24312 return Ok(());
24313 }
24314 let cte_name = cte.name.clone();
24315 let col_names = cte.column_overrides.clone();
24316 if col_names.is_empty() {
24317 return Err(
24318 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24319 );
24320 }
24321 let search = cte.search.take();
24322 let cycle = cte.cycle.take();
24323 let mut extra_cols: Vec<String> = Vec::new();
24324 let col_ref = |name: &str| {
24325 Expr::Column(ColumnName {
24326 qualifier: Some(cte_name.clone()),
24327 name: name.to_string(),
24328 })
24329 };
24330 // Position of a SEARCH/CYCLE column within the CTE's column list.
24331 let pos_of = |name: &str| -> Result<usize, ParseError> {
24332 col_names
24333 .iter()
24334 .position(|c| c.eq_ignore_ascii_case(name))
24335 .ok_or_else(|| {
24336 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24337 })
24338 };
24339 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24340 let mut args = Vec::with_capacity(positions.len());
24341 for &p in positions {
24342 match items.get(p) {
24343 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24344 _ => {
24345 return Err(self.err(
24346 "SEARCH/CYCLE column maps to a non-expression select item".into(),
24347 ));
24348 }
24349 }
24350 }
24351 Ok(Expr::FunctionCall {
24352 name: "row".into(),
24353 args,
24354 })
24355 };
24356 let CteBody::Select(body) = &mut cte.body else {
24357 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24358 };
24359 if body.unions.is_empty() {
24360 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24361 }
24362 let rec = body.unions.len() - 1; // recursive term = last UNION peer
24363
24364 if let Some(srch) = search {
24365 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24366 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24367 // no typed `record[]`, but element-wise array ORDER BY is correct
24368 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24369 // exactly onto a typed array: DEPTH is the root→node path
24370 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24371 // orders numerically (multi-digit keys included), matching PG.
24372 //
24373 // A multi-column BY would need a record[] to keep the per-node key
24374 // tuple orderable, which SPG can't express — error honestly there
24375 // rather than mis-order.
24376 if srch.by_columns.len() != 1 {
24377 return Err(self.err(
24378 "SEARCH … BY with multiple columns needs typed record[] ordering \
24379 SPG doesn't have yet; a single BY column is supported"
24380 .into(),
24381 ));
24382 }
24383 let key_pos = pos_of(&srch.by_columns[0])?;
24384 let base_key = match body.items.get(key_pos) {
24385 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24386 _ => {
24387 return Err(
24388 self.err("SEARCH BY column maps to a non-expression select item".into())
24389 );
24390 }
24391 };
24392 let rec_key = match body.unions[rec].1.items.get(key_pos) {
24393 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24394 _ => {
24395 return Err(
24396 self.err("SEARCH BY column maps to a non-expression select item".into())
24397 );
24398 }
24399 };
24400 if srch.depth_first {
24401 // base: ARRAY[key]; rec: array_append(cte.set, key).
24402 body.items.push(SelectItem::Expr {
24403 expr: Expr::Array(alloc::vec![base_key]),
24404 alias: Some(srch.set_column.clone()),
24405 });
24406 body.unions[rec].1.items.push(SelectItem::Expr {
24407 expr: Expr::FunctionCall {
24408 name: "array_append".into(),
24409 args: alloc::vec![col_ref(&srch.set_column), rec_key],
24410 },
24411 alias: Some(srch.set_column.clone()),
24412 });
24413 } else {
24414 // BREADTH: [depth, key]; depth starts at 0 and increments. The
24415 // leading depth element dominates the element-wise comparison,
24416 // so shallower rows sort first, then by key — PG's (depth, key).
24417 body.items.push(SelectItem::Expr {
24418 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24419 alias: Some(srch.set_column.clone()),
24420 });
24421 // rec depth = cte.set[1] + 1.
24422 let parent_depth = Expr::ArraySubscript {
24423 target: Box::new(col_ref(&srch.set_column)),
24424 index: Box::new(Expr::Literal(Literal::Integer(1))),
24425 };
24426 body.unions[rec].1.items.push(SelectItem::Expr {
24427 expr: Expr::Array(alloc::vec![
24428 Expr::Binary {
24429 lhs: Box::new(parent_depth),
24430 op: BinOp::Add,
24431 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24432 },
24433 rec_key,
24434 ]),
24435 alias: Some(srch.set_column.clone()),
24436 });
24437 }
24438 extra_cols.push(srch.set_column);
24439 }
24440
24441 if let Some(cyc) = cycle {
24442 let positions: Vec<usize> = cyc
24443 .columns
24444 .iter()
24445 .map(|c| pos_of(c))
24446 .collect::<Result<_, _>>()?;
24447 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24448 // cast it to text for the cycle path: membership only needs equality,
24449 // and the record text form gives SPG a TextArray path (SPG has no
24450 // typed record[] array). Cycle detection is unaffected.
24451 let base_row = Expr::Cast {
24452 expr: Box::new(row_of(&body.items, &positions)?),
24453 target: CastTarget::Text,
24454 };
24455 let rec_row = Expr::Cast {
24456 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24457 target: CastTarget::Text,
24458 };
24459 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24460 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24461 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24462 body.items.push(SelectItem::Expr {
24463 expr: Expr::Literal(dflt.clone()),
24464 alias: Some(cyc.mark_column.clone()),
24465 });
24466 body.items.push(SelectItem::Expr {
24467 expr: Expr::Array(alloc::vec![base_row]),
24468 alias: Some(cyc.path_column.clone()),
24469 });
24470 // rec mark: ROW(cols) already in the path → cycle.
24471 let hit = Expr::AnyAll {
24472 expr: Box::new(rec_row.clone()),
24473 op: BinOp::Eq,
24474 array: Box::new(col_ref(&cyc.path_column)),
24475 is_any: true,
24476 };
24477 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24478 Expr::Case {
24479 operand: None,
24480 branches: alloc::vec![(hit, Expr::Literal(mark))],
24481 else_branch: Some(Box::new(Expr::Literal(dflt))),
24482 }
24483 } else {
24484 hit
24485 };
24486 body.unions[rec].1.items.push(SelectItem::Expr {
24487 expr: mark_expr,
24488 alias: Some(cyc.mark_column.clone()),
24489 });
24490 // rec path: array_append(cte.path, ROW(cols)).
24491 body.unions[rec].1.items.push(SelectItem::Expr {
24492 expr: Expr::FunctionCall {
24493 name: "array_append".into(),
24494 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24495 },
24496 alias: Some(cyc.path_column.clone()),
24497 });
24498 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24499 let stop = Expr::Unary {
24500 op: UnOp::Not,
24501 expr: Box::new(col_ref(&cyc.mark_column)),
24502 };
24503 let w = &mut body.unions[rec].1.where_;
24504 *w = Some(match w.take() {
24505 Some(prev) => Expr::Binary {
24506 lhs: Box::new(prev),
24507 op: BinOp::And,
24508 rhs: Box::new(stop),
24509 },
24510 None => stop,
24511 });
24512 extra_cols.push(cyc.mark_column);
24513 extra_cols.push(cyc.path_column);
24514 }
24515 cte.column_overrides.extend(extra_cols);
24516 Ok(())
24517 }
24518
24519 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24520 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24521 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24522 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24523 return Ok(None);
24524 }
24525 self.advance(); // SEARCH
24526 let depth_first = match self.peek() {
24527 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24528 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24529 other => {
24530 return Err(self.err(format!(
24531 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24532 )));
24533 }
24534 };
24535 self.advance();
24536 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24537 return Err(self.err(format!(
24538 "expected FIRST after SEARCH mode, got {:?}",
24539 self.peek()
24540 )));
24541 }
24542 self.advance();
24543 if !self.peek_is_by() {
24544 return Err(self.err(format!(
24545 "expected BY after SEARCH … FIRST, got {:?}",
24546 self.peek()
24547 )));
24548 }
24549 self.advance();
24550 let mut by_columns = alloc::vec![self.expect_ident_like()?];
24551 while matches!(self.peek(), Token::Comma) {
24552 self.advance();
24553 by_columns.push(self.expect_ident_like()?);
24554 }
24555 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24556 return Err(self.err(format!(
24557 "expected SET in SEARCH clause, got {:?}",
24558 self.peek()
24559 )));
24560 }
24561 self.advance();
24562 let set_column = self.expect_ident_like()?;
24563 Ok(Some(crate::ast::SearchClause {
24564 depth_first,
24565 by_columns,
24566 set_column,
24567 }))
24568 }
24569
24570 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24571 /// USING pathcol`. Returns None when the next token isn't CYCLE.
24572 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24573 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24574 return Ok(None);
24575 }
24576 self.advance(); // CYCLE
24577 let mut columns = alloc::vec![self.expect_ident_like()?];
24578 while matches!(self.peek(), Token::Comma) {
24579 self.advance();
24580 columns.push(self.expect_ident_like()?);
24581 }
24582 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24583 return Err(self.err(format!(
24584 "expected SET in CYCLE clause, got {:?}",
24585 self.peek()
24586 )));
24587 }
24588 self.advance();
24589 let mark_column = self.expect_ident_like()?;
24590 let mut mark_value = None;
24591 let mut default_value = None;
24592 if matches!(self.peek(), Token::To) {
24593 self.advance();
24594 mark_value = Some(self.parse_cycle_literal()?);
24595 if !matches!(self.peek(), Token::Default) {
24596 return Err(self.err(format!(
24597 "expected DEFAULT after CYCLE … TO, got {:?}",
24598 self.peek()
24599 )));
24600 }
24601 self.advance();
24602 default_value = Some(self.parse_cycle_literal()?);
24603 }
24604 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24605 return Err(self.err(format!(
24606 "expected USING in CYCLE clause, got {:?}",
24607 self.peek()
24608 )));
24609 }
24610 self.advance();
24611 let path_column = self.expect_ident_like()?;
24612 Ok(Some(crate::ast::CycleClause {
24613 columns,
24614 mark_column,
24615 mark_value,
24616 default_value,
24617 path_column,
24618 }))
24619 }
24620
24621 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24622 /// literal (string / bool / number) in PG.
24623 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24624 match self.parse_expr(0)? {
24625 Expr::Literal(l) => Ok(l),
24626 other => Err(self.err(format!(
24627 "CYCLE mark/default value must be a literal, got {other:?}"
24628 ))),
24629 }
24630 }
24631
24632 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24633 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24634 // Comes through as an identifier; consume it if present and
24635 // mark every CTE in the clause as recursive (PG semantics —
24636 // the flag is per-WITH, not per-CTE).
24637 let mut recursive = false;
24638 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24639 && s.eq_ignore_ascii_case("recursive")
24640 {
24641 self.advance();
24642 recursive = true;
24643 }
24644 let mut ctes = Vec::new();
24645 loop {
24646 let name = self.expect_ident_like()?;
24647 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24648 // PG uses these to rename the body's output columns; we
24649 // do the same below by overriding `columns[i].name`.
24650 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24651 self.advance();
24652 let mut names = Vec::new();
24653 loop {
24654 names.push(self.expect_ident_like()?);
24655 if matches!(self.peek(), Token::Comma) {
24656 self.advance();
24657 continue;
24658 }
24659 break;
24660 }
24661 if !matches!(self.peek(), Token::RParen) {
24662 return Err(self.err(format!(
24663 "expected ')' to close CTE column list, got {:?}",
24664 self.peek()
24665 )));
24666 }
24667 self.advance();
24668 names
24669 } else {
24670 Vec::new()
24671 };
24672 // AS is a reserved Token::As (used by SELECT-item / FROM
24673 // aliasing) — handle it specially rather than as a bare
24674 // ident.
24675 if !matches!(self.peek(), Token::As) {
24676 return Err(self.err(format!(
24677 "expected AS after CTE name {name:?}, got {:?}",
24678 self.peek()
24679 )));
24680 }
24681 self.advance();
24682 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24683 // MATERIALIZED` optimizer hints. SPG materialises every
24684 // CTE, so both spellings are accepted and absorbed.
24685 if matches!(self.peek(), Token::Not) {
24686 self.advance(); // NOT
24687 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24688 if s.eq_ignore_ascii_case("materialized"))
24689 {
24690 self.advance();
24691 } else {
24692 return Err(self.err(format!(
24693 "expected MATERIALIZED after AS NOT, got {:?}",
24694 self.peek()
24695 )));
24696 }
24697 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24698 if s.eq_ignore_ascii_case("materialized"))
24699 {
24700 self.advance();
24701 }
24702 if !matches!(self.peek(), Token::LParen) {
24703 return Err(self.err(format!(
24704 "expected '(' after AS in WITH clause, got {:?}",
24705 self.peek()
24706 )));
24707 }
24708 self.advance();
24709 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24710 // RETURNING) as the CTE body in addition to SELECT.
24711 // PG writable CTE semantics. UPDATE / DELETE come in as
24712 // bare Idents (lexer keeps SELECT / INSERT as reserved
24713 // tokens but treats the rest of DML as case-insensitive
24714 // idents).
24715 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24716 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24717 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24718 let body = match self.peek() {
24719 Token::Select => {
24720 let inner = self.parse_select_stmt()?;
24721 let Statement::Select(s) = inner else {
24722 unreachable!("parse_select_stmt returns Select");
24723 };
24724 crate::ast::CteBody::Select(s)
24725 }
24726 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24727 // `SELECT * FROM t` this way and accepts it wherever a
24728 // SELECT goes, so the CTE body dispatch needs its own
24729 // arm: this match is keyed on the FIRST token, and
24730 // `Token::Table` fell through to a tail that then
24731 // rejected what it got. `parse_table_shorthand` has
24732 // returned a desugared SelectStatement since the
24733 // shorthand landed — only the routing was missing.
24734 // Round 868 found this by putting the shorthand in a
24735 // subquery; every earlier check used a top-level form.
24736 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24737 // `SELECT * FROM t` this way and accepts it wherever a
24738 // SELECT goes, so the CTE body dispatch needs its own
24739 // arm: this match is keyed on the FIRST token, and
24740 // `Token::Table` fell through to a tail that rejected
24741 // what it got. `parse_table_shorthand` has returned a
24742 // desugared SelectStatement since the shorthand landed —
24743 // only the routing was missing, here and in the derived
24744 // table's second-token gate. Round 868 found both by
24745 // putting the shorthand in a subquery; every earlier
24746 // check had used a top-level form.
24747 Token::Table
24748 if matches!(
24749 self.tokens.get(self.pos + 1),
24750 Some(Token::Ident(_) | Token::QuotedIdent(_))
24751 ) =>
24752 {
24753 let mut head = self.parse_table_shorthand()?;
24754 self.parse_setop_chain_into(&mut head)?;
24755 self.parse_select_tail_into(&mut head)?;
24756 crate::ast::CteBody::Select(head)
24757 }
24758 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24759 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24760 // the shared rows helper onto a Select body.
24761 Token::Values => {
24762 self.advance(); // VALUES
24763 let mut head = self.parse_values_rows_body()?;
24764 // A VALUES seed can head a set-operation chain —
24765 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24766 // SELECT n+1 FROM t …). Attach any trailing
24767 // UNION / INTERSECT / EXCEPT peers so the
24768 // recursive-CTE body parses like the SELECT seed.
24769 self.parse_setop_chain_into(&mut head)?;
24770 crate::ast::CteBody::Select(head)
24771 }
24772 Token::Insert => {
24773 let inner = self.parse_one_statement()?;
24774 let Statement::Insert(s) = inner else {
24775 unreachable!("Token::Insert routes to Insert");
24776 };
24777 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24778 }
24779 _ if is_update_kw => {
24780 let inner = self.parse_one_statement()?;
24781 let Statement::Update(s) = inner else {
24782 return Err(
24783 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24784 );
24785 };
24786 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24787 }
24788 _ if is_delete_kw => {
24789 let inner = self.parse_one_statement()?;
24790 let Statement::Delete(s) = inner else {
24791 return Err(
24792 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24793 );
24794 };
24795 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24796 }
24797 // v7.39 (round 149) — PG 17 allows MERGE as a
24798 // data-modifying CTE body.
24799 _ if is_merge_kw => {
24800 let inner = self.parse_one_statement()?;
24801 let Statement::Merge(s) = inner else {
24802 return Err(
24803 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24804 );
24805 };
24806 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24807 }
24808 // v7.39 (round 151) — a CTE body may itself be
24809 // WITH-headed (PG grammar: PreparableStmt carries its
24810 // own with_clause). The nested statement keeps its own
24811 // ctes; the modifying-CTE-at-top-level rule is enforced
24812 // at execution.
24813 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24814 self.advance(); // WITH
24815 match self.parse_with_cte_then_select()? {
24816 Statement::Select(s) => crate::ast::CteBody::Select(s),
24817 Statement::Insert(s) => {
24818 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24819 }
24820 Statement::Update(s) => {
24821 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24822 }
24823 Statement::Delete(s) => {
24824 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24825 }
24826 Statement::Merge(s) => {
24827 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24828 }
24829
24830 other => {
24831 return Err(self.err(format!(
24832 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24833 )));
24834 }
24835 }
24836 }
24837 other => {
24838 return Err(self.err(format!(
24839 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24840 )));
24841 }
24842 };
24843 if !matches!(self.peek(), Token::RParen) {
24844 return Err(self.err(format!(
24845 "expected ')' after CTE body, got {:?}",
24846 self.peek()
24847 )));
24848 }
24849 self.advance();
24850 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24851 // CTE, desugared into extra body columns by the engine.
24852 let search = self.parse_cte_search_clause()?;
24853 let cycle = self.parse_cte_cycle_clause()?;
24854 let mut cte = crate::ast::Cte {
24855 name,
24856 body,
24857 recursive,
24858 column_overrides,
24859 search,
24860 cycle,
24861 };
24862 self.validate_recursive_cte(&cte)?;
24863 self.desugar_cte_search_cycle(&mut cte)?;
24864 ctes.push(cte);
24865 if matches!(self.peek(), Token::Comma) {
24866 self.advance();
24867 continue;
24868 }
24869 break;
24870 }
24871 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24872 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24873 // the parsed CTEs to whichever statement the body produces.
24874 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24875 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24876 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24877 match self.peek() {
24878 Token::Select => {
24879 let body_stmt = self.parse_select_stmt()?;
24880 let Statement::Select(mut body) = body_stmt else {
24881 unreachable!()
24882 };
24883 body.ctes = ctes;
24884 Ok(Statement::Select(body))
24885 }
24886 Token::Insert => {
24887 let body_stmt = self.parse_one_statement()?;
24888 let Statement::Insert(mut body) = body_stmt else {
24889 unreachable!()
24890 };
24891 body.ctes = ctes;
24892 Ok(Statement::Insert(body))
24893 }
24894 _ if outer_is_update => {
24895 let body_stmt = self.parse_one_statement()?;
24896 let Statement::Update(mut body) = body_stmt else {
24897 return Err(self.err(format!("expected UPDATE after WITH clause")));
24898 };
24899 body.ctes = ctes;
24900 Ok(Statement::Update(body))
24901 }
24902 _ if outer_is_delete => {
24903 let body_stmt = self.parse_one_statement()?;
24904 let Statement::Delete(mut body) = body_stmt else {
24905 return Err(self.err(format!("expected DELETE after WITH clause")));
24906 };
24907 body.ctes = ctes;
24908 Ok(Statement::Delete(body))
24909 }
24910 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24911 // WITH RECURSIVE is rejected with PG's exact message
24912 // (parse analysis, transformWithClause).
24913 _ if outer_is_merge => {
24914 if recursive {
24915 return Err(self.err(String::from(
24916 "WITH RECURSIVE is not supported for MERGE statement",
24917 )));
24918 }
24919 let body_stmt = self.parse_one_statement()?;
24920 let Statement::Merge(mut body) = body_stmt else {
24921 return Err(self.err(format!("expected MERGE after WITH clause")));
24922 };
24923 body.ctes = ctes;
24924 Ok(Statement::Merge(body))
24925 }
24926 other => Err(self.err(format!(
24927 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24928 ))),
24929 }
24930 }
24931
24932 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24933 /// already consumed the leading `EXISTS` ident via
24934 /// `self.advance()`.
24935 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24936 /// the leading `CASE` ident has been consumed (mailrs round-5
24937 /// G9). Supports both the searched form
24938 /// (`CASE WHEN cond THEN val …`) and the simple form
24939 /// (`CASE operand WHEN val THEN val …`).
24940 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24941 // Disambiguate searched vs simple form: if the next token
24942 // is `WHEN`, we're in the searched form. Otherwise the
24943 // intervening expression is the operand.
24944 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24945 None
24946 } else {
24947 Some(Box::new(self.parse_expr(0)?))
24948 };
24949 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24950 loop {
24951 match self.peek() {
24952 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24953 self.advance();
24954 let cond = self.parse_expr(0)?;
24955 match self.peek() {
24956 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24957 self.advance();
24958 }
24959 other => {
24960 return Err(self.err(alloc::format!(
24961 "expected THEN after CASE WHEN <expr>, got {other:?}"
24962 )));
24963 }
24964 }
24965 let value = self.parse_expr(0)?;
24966 branches.push((cond, value));
24967 }
24968 _ => break,
24969 }
24970 }
24971 if branches.is_empty() {
24972 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24973 }
24974 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24975 {
24976 self.advance();
24977 Some(Box::new(self.parse_expr(0)?))
24978 } else {
24979 None
24980 };
24981 match self.peek() {
24982 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24983 self.advance();
24984 }
24985 other => {
24986 return Err(self.err(alloc::format!(
24987 "expected END to close CASE expression, got {other:?}"
24988 )));
24989 }
24990 }
24991 Ok(Expr::Case {
24992 operand,
24993 branches,
24994 else_branch,
24995 })
24996 }
24997
24998 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24999 /// query-source position (EXISTS / IN / INSERT source / CTE body /
25000 /// view body). Caller consumed the WITH keyword. Only a SELECT
25001 /// outer is grammatical here; the data-modifying-CTE-at-top-level
25002 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25003 /// maps correctly.
25004 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25005 let inner = self.parse_with_cte_then_select()?;
25006 match inner {
25007 Statement::Select(s) => Ok(s),
25008 other => Err(self.err(format!(
25009 "expected SELECT after WITH in a subquery, got {other:?}"
25010 ))),
25011 }
25012 }
25013
25014 /// True when the next token is the (unquoted) WITH keyword. WITH is
25015 /// reserved in PG, so a bare `with` can never be a column reference
25016 /// in these positions; a quoted `"with"` stays an identifier.
25017 fn peek_is_with_kw(&self) -> bool {
25018 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25019 }
25020
25021 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25022 /// `#[inline(never)]` keeps the large SelectStatement temporaries
25023 /// off parse_expr's recursive frame (the nesting-budget stack
25024 /// cliff — see the round-153 gate regression).
25025 #[inline(never)]
25026 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25027 if self.peek_is_with_kw() {
25028 self.advance();
25029 self.parse_nested_with_select()
25030 } else {
25031 match self.parse_select_stmt()? {
25032 Statement::Select(s) => Ok(s),
25033 other => Err(self.err(alloc::format!(
25034 "expected SELECT inside ANY/ALL, got {other:?}"
25035 ))),
25036 }
25037 }
25038 }
25039
25040 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25041 if !matches!(self.peek(), Token::LParen) {
25042 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25043 }
25044 self.advance();
25045 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25046 let s = if self.peek_is_with_kw() {
25047 self.advance();
25048 self.parse_nested_with_select()?
25049 } else {
25050 let inner = self.parse_select_stmt()?;
25051 let Statement::Select(s) = inner else {
25052 unreachable!("parse_select_stmt returns Select")
25053 };
25054 s
25055 };
25056 if !matches!(self.peek(), Token::RParen) {
25057 return Err(self.err(format!(
25058 "expected ')' after EXISTS-subquery, got {:?}",
25059 self.peek()
25060 )));
25061 }
25062 self.advance();
25063 Ok(Expr::Exists {
25064 subquery: Box::new(s),
25065 negated,
25066 })
25067 }
25068
25069 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25070 self.advance(); // IN
25071 if !matches!(self.peek(), Token::LParen) {
25072 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25073 }
25074 self.advance();
25075 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25076 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25077 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25078 let s = if self.peek_is_with_kw() {
25079 self.advance();
25080 self.parse_nested_with_select()?
25081 } else {
25082 let inner = self.parse_select_stmt()?;
25083 let Statement::Select(s) = inner else {
25084 unreachable!("parse_select_stmt always returns Statement::Select")
25085 };
25086 s
25087 };
25088 if !matches!(self.peek(), Token::RParen) {
25089 return Err(self.err(format!(
25090 "expected ')' after IN-subquery, got {:?}",
25091 self.peek()
25092 )));
25093 }
25094 self.advance();
25095 return Ok(Expr::InSubquery {
25096 expr: Box::new(expr),
25097 subquery: Box::new(s),
25098 negated,
25099 });
25100 }
25101 let mut elements = Vec::new();
25102 if !matches!(self.peek(), Token::RParen) {
25103 loop {
25104 elements.push(self.parse_expr(0)?);
25105 match self.peek() {
25106 Token::Comma => {
25107 self.advance();
25108 }
25109 Token::RParen => break,
25110 other => {
25111 return Err(
25112 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25113 );
25114 }
25115 }
25116 }
25117 }
25118 self.advance(); // ')'
25119 // v7.30.2 (mailrs round-25) — flat InList node instead of a
25120 // left-deep OR-Eq chain: chain depth scaled with the element
25121 // count and overflowed the stack (eval + drop are recursive).
25122 if elements.is_empty() {
25123 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25124 }
25125 Ok(Expr::InList {
25126 expr: Box::new(expr),
25127 list: elements,
25128 negated,
25129 })
25130 }
25131
25132 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25133 /// already consumed by the caller. Elements must be numeric literals
25134 /// (with optional unary `-`); any compound expression is rejected at
25135 /// parse time so the runtime never needs to evaluate inside a vector.
25136 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25137 /// has already consumed the `EXTRACT` token before calling us —
25138 /// we pick up at the opening `(`.
25139 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25140 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25141 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25142 /// per-column OR-fold of
25143 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25144 /// term)` so the existing FTS evaluator handles semantics.
25145 ///
25146 /// The mode modifier is accepted-and-ignored at v7.17 — all
25147 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25148 /// mode operators (`+foo -bar`) would need their own parser
25149 /// (Phase 2.2c); customers who hit them today already get a
25150 /// correct lexeme-match against the bare term, only without
25151 /// the +/- precedence the customer asked for.
25152 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25153 // Already at `MATCH`-consumed position; the dispatcher
25154 // confirmed the next token is `(`.
25155 if !matches!(self.peek(), Token::LParen) {
25156 return Err(self.err(alloc::format!(
25157 "expected '(' after MATCH, got {:?}",
25158 self.peek()
25159 )));
25160 }
25161 self.advance();
25162 let mut cols: Vec<Expr> = Vec::new();
25163 loop {
25164 cols.push(self.parse_expr(0)?);
25165 match self.peek() {
25166 Token::Comma => {
25167 self.advance();
25168 }
25169 Token::RParen => break,
25170 other => {
25171 return Err(self.err(alloc::format!(
25172 "expected ',' or ')' in MATCH column list, got {other:?}"
25173 )));
25174 }
25175 }
25176 }
25177 self.advance(); // ')'
25178 // Expect AGAINST.
25179 match self.peek() {
25180 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25181 self.advance();
25182 }
25183 other => {
25184 return Err(self.err(alloc::format!(
25185 "expected AGAINST after MATCH column list, got {other:?}"
25186 )));
25187 }
25188 }
25189 if !matches!(self.peek(), Token::LParen) {
25190 return Err(self.err(alloc::format!(
25191 "expected '(' after AGAINST, got {:?}",
25192 self.peek()
25193 )));
25194 }
25195 self.advance();
25196 // Read AGAINST's argument as a single primary token —
25197 // string literal, placeholder, or column-ref ident. We
25198 // can't call `parse_expr` / `parse_unary` here because
25199 // the postfix chain inside `parse_atom` would greedily
25200 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25201 // and fail at "expected '(' after IN". Customers always
25202 // write a literal or bound parameter in AGAINST, so this
25203 // restriction is non-blocking; the error path explains
25204 // the limit if a more complex expression shows up.
25205 let term = match self.advance() {
25206 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25207 Token::Placeholder(n) => Expr::Placeholder(n),
25208 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25209 qualifier: None,
25210 name: s,
25211 }),
25212 other => {
25213 return Err(self.err(alloc::format!(
25214 "MATCH ... AGAINST(<term>) expects a string literal, \
25215 bound parameter, or column ref, got {other:?}"
25216 )));
25217 }
25218 };
25219 // Optional mode tail — accept-and-ignore at v7.17:
25220 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25221 // IN BOOLEAN MODE
25222 // WITH QUERY EXPANSION
25223 loop {
25224 match self.peek() {
25225 // IN lexes as a reserved Token::In, not an ident,
25226 // so it gets its own arm.
25227 Token::In => {
25228 self.advance();
25229 }
25230 Token::Ident(s) | Token::QuotedIdent(s)
25231 if s.eq_ignore_ascii_case("natural")
25232 || s.eq_ignore_ascii_case("language")
25233 || s.eq_ignore_ascii_case("boolean")
25234 || s.eq_ignore_ascii_case("mode")
25235 || s.eq_ignore_ascii_case("with")
25236 || s.eq_ignore_ascii_case("query")
25237 || s.eq_ignore_ascii_case("expansion") =>
25238 {
25239 self.advance();
25240 }
25241 _ => break,
25242 }
25243 }
25244 if !matches!(self.peek(), Token::RParen) {
25245 return Err(self.err(alloc::format!(
25246 "expected ')' to close AGAINST, got {:?}",
25247 self.peek()
25248 )));
25249 }
25250 self.advance();
25251 // Build per-column `to_tsvector('simple', col) @@
25252 // plainto_tsquery('simple', term)` and OR-fold.
25253 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25254 let plainto = Expr::FunctionCall {
25255 name: String::from("plainto_tsquery"),
25256 args: alloc::vec![simple_lit(), term.clone()],
25257 };
25258 let mut folded: Option<Expr> = None;
25259 for col in cols {
25260 let to_tsv = Expr::FunctionCall {
25261 name: String::from("to_tsvector"),
25262 args: alloc::vec![simple_lit(), col],
25263 };
25264 let leaf = Expr::Binary {
25265 lhs: Box::new(to_tsv),
25266 op: crate::ast::BinOp::TsMatch,
25267 rhs: Box::new(plainto.clone()),
25268 };
25269 folded = Some(match folded {
25270 None => leaf,
25271 Some(prev) => Expr::Binary {
25272 lhs: Box::new(prev),
25273 op: crate::ast::BinOp::Or,
25274 rhs: Box::new(leaf),
25275 },
25276 });
25277 }
25278 match folded {
25279 Some(e) => Ok(e),
25280 None => Err(self.err(String::from(
25281 "MATCH(...) AGAINST(...) requires at least one column",
25282 ))),
25283 }
25284 }
25285
25286 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25287 if !matches!(self.peek(), Token::LParen) {
25288 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25289 }
25290 self.advance();
25291 let field_name = self.expect_ident_like()?;
25292 let field = match field_name.to_ascii_lowercase().as_str() {
25293 // PG accepts the plural spellings (years/months/…/millenniums) as
25294 // aliases for the singular fields — its datetime unit table has both.
25295 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25296 "year" | "years" => ExtractField::Year,
25297 "month" | "months" => ExtractField::Month,
25298 "day" | "days" => ExtractField::Day,
25299 "hour" | "hours" => ExtractField::Hour,
25300 "minute" | "minutes" => ExtractField::Minute,
25301 "second" | "seconds" => ExtractField::Second,
25302 "microsecond" | "microseconds" => ExtractField::Microsecond,
25303 "epoch" => ExtractField::Epoch,
25304 "dow" => ExtractField::Dow,
25305 "isodow" => ExtractField::Isodow,
25306 "doy" => ExtractField::Doy,
25307 "week" | "weeks" => ExtractField::Week,
25308 "isoyear" => ExtractField::Isoyear,
25309 "quarter" => ExtractField::Quarter,
25310 "decade" | "decades" => ExtractField::Decade,
25311 "century" | "centuries" => ExtractField::Century,
25312 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25313 "julian" => ExtractField::Julian,
25314 "millisecond" | "milliseconds" => ExtractField::Millisecond,
25315 "timezone" => ExtractField::Timezone,
25316 "timezone_hour" => ExtractField::TimezoneHour,
25317 "timezone_minute" => ExtractField::TimezoneMinute,
25318 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25319 // reports an unknown one with the source type (22023); carry the
25320 // raw name so eval can word it.
25321 other => ExtractField::Other(alloc::string::String::from(other)),
25322 };
25323 if !matches!(self.peek(), Token::From) {
25324 return Err(self.err(format!(
25325 "expected FROM after EXTRACT field, got {:?}",
25326 self.peek()
25327 )));
25328 }
25329 self.advance();
25330 let source = self.parse_expr(0)?;
25331 if !matches!(self.peek(), Token::RParen) {
25332 return Err(self.err(format!(
25333 "expected ')' to close EXTRACT, got {:?}",
25334 self.peek()
25335 )));
25336 }
25337 self.advance();
25338 Ok(Expr::Extract {
25339 field,
25340 source: Box::new(source),
25341 })
25342 }
25343
25344 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25345 /// is already consumed; we expect a single string literal next and
25346 /// resolve it into `Literal::Interval` at parse time so the engine
25347 /// never has to re-tokenise inside the string.
25348 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25349 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25350 /// is the SQL-standard form and is left to the path below.
25351 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25352 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25353 let (offset, sign) = match self.peek() {
25354 Token::Minus => (1, "-"),
25355 _ => (0, ""),
25356 };
25357 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25358 return None;
25359 };
25360 self.tokens
25361 .get(self.pos + offset + 1)
25362 .filter(|t| mysql_interval_unit(t).is_some())?;
25363 Some((alloc::format!("{sign}{n}"), offset + 1))
25364 }
25365
25366 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25367 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25368 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25369 ///
25370 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25371 /// this by parsing the group and then restoring `self.pos` — which could
25372 /// never have worked, because `advance()` DESTROYS the token it returns
25373 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25374 /// inert only because both branches errored back then.
25375 fn interval_paren_is_quantity(&self) -> bool {
25376 let mut depth = 0usize;
25377 let mut saw_top_level_comma = false;
25378 let mut i = self.pos;
25379 while let Some(tok) = self.tokens.get(i) {
25380 match tok {
25381 Token::LParen => depth += 1,
25382 Token::RParen => {
25383 depth = depth.saturating_sub(1);
25384 if depth == 0 {
25385 return !saw_top_level_comma
25386 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25387 .is_some();
25388 }
25389 }
25390 // A comma directly inside the outermost parens means the
25391 // argument list of the INTERVAL() function.
25392 Token::Comma if depth == 1 => saw_top_level_comma = true,
25393 Token::Eof => return false,
25394 _ => {}
25395 }
25396 i += 1;
25397 }
25398 false
25399 }
25400
25401 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25402 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25403 // (the index of the last Ni ≤ N), distinct from the interval literal.
25404 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25405 // is decided by a non-destructive lookahead (round 422) before either
25406 // branch consumes anything. MySQL only.
25407 if self.mysql_dialect
25408 && matches!(self.peek(), Token::LParen)
25409 && !self.interval_paren_is_quantity()
25410 {
25411 self.advance(); // (
25412 let mut args = Vec::new();
25413 if !matches!(self.peek(), Token::RParen) {
25414 loop {
25415 args.push(self.parse_expr(0)?);
25416 if matches!(self.peek(), Token::Comma) {
25417 self.advance();
25418 continue;
25419 }
25420 break;
25421 }
25422 }
25423 if !matches!(self.peek(), Token::RParen) {
25424 return Err(self.err(alloc::format!(
25425 "expected ')' after INTERVAL() arguments, got {:?}",
25426 self.peek()
25427 )));
25428 }
25429 self.advance(); // )
25430 return Ok(Expr::FunctionCall {
25431 name: alloc::string::String::from("interval"),
25432 args,
25433 });
25434 }
25435 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25436 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25437 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25438 // writes every date arithmetic there is, and it did not parse at
25439 // all. PG rejects the unquoted form outright (`syntax error at or
25440 // near "1"`, measured), so it is taken only in the MySQL dialect —
25441 // PG's own `INTERVAL '1' DAY` is untouched below.
25442 if self.mysql_dialect
25443 && let Some((text, consume)) = self.peek_unquoted_interval_count()
25444 {
25445 for _ in 0..consume {
25446 self.advance(); // the optional `-` and the number
25447 }
25448 let Some(unit) = mysql_interval_unit(self.peek()) else {
25449 return Err(self.err(alloc::format!(
25450 "expected an interval unit after INTERVAL {text}, got {:?}",
25451 self.peek()
25452 )));
25453 };
25454 self.advance(); // the unit
25455 let (months, days, micros) = scale_mysql_interval(&text, unit)
25456 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25457 return Ok(Expr::Literal(Literal::Interval {
25458 months,
25459 days,
25460 micros,
25461 // The canonical rendering, so Display round-trips into a
25462 // form both dialects read back.
25463 text: alloc::format!("{text} {unit}"),
25464 }));
25465 }
25466 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25467 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25468 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25469 // Those cannot fold into a compile-time `Literal::Interval`, so they
25470 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25471 // builtin, which builds the value at run time (and yields NULL for a
25472 // NULL quantity, as MariaDB does). The literal path above still folds
25473 // the constant case — it is cheaper and round-trips through Display.
25474 //
25475 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25476 // MySQL's quoted spelling) keep the qualifier path below.
25477 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25478 let qty = self.parse_expr(0)?;
25479 let Some(unit) = mysql_interval_unit(self.peek()) else {
25480 return Err(self.err(alloc::format!(
25481 "expected an interval unit after INTERVAL <expr>, got {:?}",
25482 self.peek()
25483 )));
25484 };
25485 self.advance(); // the unit
25486 return Ok(make_interval_call(qty, unit));
25487 }
25488 let tok = self.advance();
25489 let Token::String(text) = tok else {
25490 return Err(self.err(format!(
25491 "expected string literal after INTERVAL, got {tok:?}"
25492 )));
25493 };
25494 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25495 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25496 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25497 // bare number means and the leading/trailing precision.
25498 let field1 = interval_field_of(self.peek());
25499 let qualifier = if let Some(f1) = field1 {
25500 self.advance();
25501 let f2 = if matches!(self.peek(), Token::To) {
25502 self.advance();
25503 let Some(f) = interval_field_of(self.peek()) else {
25504 return Err(self.err(format!(
25505 "expected an interval field after TO, got {:?}",
25506 self.peek()
25507 )));
25508 };
25509 self.advance();
25510 Some(f)
25511 } else {
25512 None
25513 };
25514 Some((f1, f2))
25515 } else {
25516 None
25517 };
25518 let (months, days, micros) = match qualifier {
25519 Some(q) => interpret_qualified_interval(&text, q),
25520 None => parse_interval_text(&text),
25521 }
25522 .ok_or_else(|| ParseError {
25523 message: format!(
25524 "cannot parse INTERVAL {text:?}; \
25525 expected `<n> <unit> [<n> <unit> ...]` with units \
25526 microsecond[s], millisecond[s], second[s], minute[s], \
25527 hour[s], day[s], week[s], month[s], year[s]"
25528 ),
25529 token_pos: self.consumed_pos(),
25530 })?;
25531 Ok(Expr::Literal(Literal::Interval {
25532 months,
25533 days,
25534 micros,
25535 text,
25536 }))
25537 }
25538
25539 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25540 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25541 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25542 /// than a pgvector literal.
25543 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25544 self.advance(); // consume `[`
25545 let mut items: Vec<Expr> = Vec::new();
25546 if !matches!(self.peek(), Token::RBracket) {
25547 loop {
25548 if matches!(self.peek(), Token::LBracket) {
25549 items.push(self.parse_array_bracket_body()?);
25550 } else {
25551 items.push(self.parse_expr(0)?);
25552 }
25553 match self.peek() {
25554 Token::Comma => {
25555 self.advance();
25556 }
25557 Token::RBracket => break,
25558 other => {
25559 return Err(self.err(alloc::format!(
25560 "expected ',' or ']' in array literal, got {other:?}"
25561 )));
25562 }
25563 }
25564 }
25565 }
25566 self.advance(); // consume `]`
25567 Ok(Expr::Array(items))
25568 }
25569
25570 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25571 let mut elems = Vec::new();
25572 if matches!(self.peek(), Token::RBracket) {
25573 self.advance();
25574 return Ok(Expr::Literal(Literal::Vector(elems)));
25575 }
25576 loop {
25577 let e = self.parse_expr(0)?;
25578 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25579 message: format!("vector element must be a numeric literal, got {e:?}"),
25580 token_pos: self.pos,
25581 })?;
25582 elems.push(x);
25583 match self.peek() {
25584 Token::Comma => {
25585 self.advance();
25586 }
25587 Token::RBracket => {
25588 self.advance();
25589 break;
25590 }
25591 other => {
25592 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25593 }
25594 }
25595 }
25596 Ok(Expr::Literal(Literal::Vector(elems)))
25597 }
25598
25599 /// Atom that started with an identifier: could be `t.col`, `col`, or
25600 /// `func(arg, ...)`. Detect each shape by looking at the next token.
25601 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25602 /// [, ...])`. Caller has already consumed `OVER`. Either clause
25603 /// is optional; an empty `()` is also legal (PG semantics).
25604 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25605 /// modifier between `name(args)` and `OVER (...)`. Default is
25606 /// `Respect`. Unrecognised idents leave the stream unchanged.
25607 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25608 let Token::Ident(s) = self.peek().clone() else {
25609 return NullTreatment::Respect;
25610 };
25611 let is_ignore = s.eq_ignore_ascii_case("ignore");
25612 let is_respect = s.eq_ignore_ascii_case("respect");
25613 if !is_ignore && !is_respect {
25614 return NullTreatment::Respect;
25615 }
25616 // Lookahead for NULLS — only consume both tokens together.
25617 // pos+1 must hold a "nulls" ident.
25618 if self.pos + 1 < self.tokens.len()
25619 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25620 && s2.eq_ignore_ascii_case("nulls")
25621 {
25622 self.advance();
25623 self.advance();
25624 return if is_ignore {
25625 NullTreatment::Ignore
25626 } else {
25627 NullTreatment::Respect
25628 };
25629 }
25630 NullTreatment::Respect
25631 }
25632
25633 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25634 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25635 /// (same shape as the `OVER` tail). Consumes the whole clause and
25636 /// returns the predicate; returns `None` when no `FILTER` follows.
25637 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25638 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25639 return Ok(None);
25640 };
25641 if !s.eq_ignore_ascii_case("filter") {
25642 return Ok(None);
25643 }
25644 self.advance(); // FILTER
25645 if !matches!(self.peek(), Token::LParen) {
25646 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25647 }
25648 self.advance(); // (
25649 if !matches!(self.peek(), Token::Where) {
25650 return Err(self.err(format!(
25651 "expected WHERE inside FILTER (...), got {:?}",
25652 self.peek()
25653 )));
25654 }
25655 self.advance(); // WHERE
25656 let cond = self.parse_expr(0)?;
25657 if !matches!(self.peek(), Token::RParen) {
25658 return Err(self.err(format!(
25659 "expected ')' to close FILTER (WHERE ...), got {:?}",
25660 self.peek()
25661 )));
25662 }
25663 self.advance(); // )
25664 Ok(Some(Box::new(cond)))
25665 }
25666
25667 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25668 /// the separator as the aggregate's second argument, which is the
25669 /// shape `string_agg` already takes. Returns whether one was there.
25670 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25671 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25672 return Ok(false);
25673 }
25674 self.advance();
25675 let Token::String(sep) = self.peek().clone() else {
25676 return Err(self.err(alloc::format!(
25677 "expected a string literal after SEPARATOR, got {:?}",
25678 self.peek()
25679 )));
25680 };
25681 self.advance();
25682 args.push(Expr::Literal(Literal::String(sep)));
25683 Ok(true)
25684 }
25685
25686 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25687 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25688 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25689 /// keys, or an empty vec when no `WITHIN GROUP` follows.
25690 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25691 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25692 return Ok(Vec::new());
25693 };
25694 if !s.eq_ignore_ascii_case("within") {
25695 return Ok(Vec::new());
25696 }
25697 self.advance(); // WITHIN
25698 if !matches!(self.peek(), Token::Group) {
25699 return Err(self.err(format!(
25700 "expected GROUP after WITHIN, got {:?}",
25701 self.peek()
25702 )));
25703 }
25704 self.advance(); // GROUP
25705 if !matches!(self.peek(), Token::LParen) {
25706 return Err(self.err(format!(
25707 "expected '(' after WITHIN GROUP, got {:?}",
25708 self.peek()
25709 )));
25710 }
25711 self.advance(); // (
25712 if !matches!(self.peek(), Token::Order) {
25713 return Err(self.err(format!(
25714 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25715 self.peek()
25716 )));
25717 }
25718 self.advance(); // ORDER
25719 if !self.peek_is_by() {
25720 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25721 }
25722 self.advance(); // BY
25723 let mut keys: Vec<OrderBy> = Vec::new();
25724 loop {
25725 // v7.39 (round 691) — save/restore, the discipline this parser
25726 // already uses around `pending_sample_preds`, so a subquery inside
25727 // a key neither inherits nor leaks the channel.
25728 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25729 let saved_coll = self.order_key_collation.take();
25730 let parsed = self.parse_expr(0);
25731 self.in_order_by_key = saved_flag;
25732 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25733 let expr = parsed?;
25734 let desc = if matches!(self.peek(), Token::Desc) {
25735 self.advance();
25736 true
25737 } else if matches!(self.peek(), Token::Asc) {
25738 self.advance();
25739 false
25740 } else {
25741 false
25742 };
25743 let nulls_first = self.parse_optional_nulls_placement()?;
25744 keys.push(OrderBy {
25745 expr,
25746 desc,
25747 nulls_first,
25748 collation,
25749 });
25750 if matches!(self.peek(), Token::Comma) {
25751 self.advance();
25752 } else {
25753 break;
25754 }
25755 }
25756 if !matches!(self.peek(), Token::RParen) {
25757 return Err(self.err(format!(
25758 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25759 self.peek()
25760 )));
25761 }
25762 self.advance(); // )
25763 Ok(keys)
25764 }
25765
25766 /// No frame clause is supported.
25767 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25768 fn parse_over_clause(
25769 &mut self,
25770 ) -> Result<
25771 (
25772 Vec<Expr>,
25773 Vec<(Expr, bool, Option<bool>)>,
25774 Option<WindowFrame>,
25775 ),
25776 ParseError,
25777 > {
25778 // `OVER w` — a named-window reference. The WINDOW clause
25779 // parses after the select list, so the name rides out as a
25780 // marker in partition_by; parse_bare_select substitutes the
25781 // definition once the clause is known.
25782 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25783 let name = w.clone();
25784 self.advance();
25785 return Ok((
25786 alloc::vec![Expr::Column(crate::ast::ColumnName {
25787 qualifier: Some("__named_window__".to_string()),
25788 name,
25789 })],
25790 Vec::new(),
25791 None,
25792 ));
25793 }
25794 if !matches!(self.peek(), Token::LParen) {
25795 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25796 }
25797 self.advance();
25798 let mut partition_by = Vec::new();
25799 let mut order_by = Vec::new();
25800 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25801 // window, refined in place. PG's rules (probed against 18.4) differ
25802 // from the bare `OVER w1` form, so the reference rides out under its
25803 // own marker and `substitute_named_windows` applies them. The base
25804 // name is any leading identifier that isn't a window-spec keyword.
25805 let base_window = match self.peek() {
25806 Token::Ident(s) | Token::QuotedIdent(s)
25807 if !s.eq_ignore_ascii_case("partition")
25808 && !s.eq_ignore_ascii_case("rows")
25809 && !s.eq_ignore_ascii_case("range")
25810 && !s.eq_ignore_ascii_case("groups") =>
25811 {
25812 let n = s.clone();
25813 self.advance();
25814 Some(n)
25815 }
25816 _ => None,
25817 };
25818 // PARTITION BY ?
25819 // v7.37.6-B promoted PARTITION to a reserved keyword
25820 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25821 // `Token::Ident("partition")`. Accept both so older sources
25822 // and the new lexer surface land on the same path.
25823 let is_partition_kw = match self.peek() {
25824 Token::Partition => true,
25825 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25826 _ => false,
25827 };
25828 if is_partition_kw {
25829 self.advance();
25830 if !self.peek_is_by() {
25831 return Err(self.err(format!(
25832 "expected BY after PARTITION, got {:?}",
25833 self.peek()
25834 )));
25835 }
25836 self.advance();
25837 loop {
25838 partition_by.push(self.parse_expr(0)?);
25839 if matches!(self.peek(), Token::Comma) {
25840 self.advance();
25841 continue;
25842 }
25843 break;
25844 }
25845 }
25846 // ORDER BY ?
25847 if matches!(self.peek(), Token::Order) {
25848 self.advance();
25849 if !self.peek_is_by() {
25850 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25851 }
25852 self.advance();
25853 loop {
25854 let e = self.parse_expr(0)?;
25855 let desc = if matches!(self.peek(), Token::Desc) {
25856 self.advance();
25857 true
25858 } else if matches!(self.peek(), Token::Asc) {
25859 self.advance();
25860 false
25861 } else {
25862 false
25863 };
25864 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25865 let nulls_first = self.parse_optional_nulls_placement()?;
25866 order_by.push((e, desc, nulls_first));
25867 if matches!(self.peek(), Token::Comma) {
25868 self.advance();
25869 continue;
25870 }
25871 break;
25872 }
25873 }
25874 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25875 // Both keywords come through the lexer as identifiers; match
25876 // case-insensitively.
25877 let mut frame: Option<WindowFrame> = None;
25878 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25879 let kind = if s.eq_ignore_ascii_case("rows") {
25880 Some(FrameKind::Rows)
25881 } else if s.eq_ignore_ascii_case("range") {
25882 Some(FrameKind::Range)
25883 } else if s.eq_ignore_ascii_case("groups") {
25884 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25885 Some(FrameKind::Groups)
25886 } else {
25887 None
25888 };
25889 if let Some(kind) = kind {
25890 self.advance();
25891 frame = Some(self.parse_frame_tail(kind)?);
25892 }
25893 }
25894 if !matches!(self.peek(), Token::RParen) {
25895 return Err(self.err(format!(
25896 "expected ')' to close OVER clause, got {:?}",
25897 self.peek()
25898 )));
25899 }
25900 self.advance();
25901 if let Some(base) = base_window {
25902 // A copy may refine but never override the base's partitioning
25903 // (PG rejects it outright, before looking the name up).
25904 if !partition_by.is_empty() {
25905 return Err(self.err(alloc::format!(
25906 "cannot override PARTITION BY clause of window \"{base}\""
25907 )));
25908 }
25909 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25910 qualifier: Some("__named_window_ref__".to_string()),
25911 name: base,
25912 })];
25913 }
25914 Ok((partition_by, order_by, frame))
25915 }
25916
25917 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25918 /// or `RANGE` keyword was just consumed. Accepts both
25919 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25920 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25921 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25922 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25923 let (start, end) = if matches!(self.peek(), Token::Between) {
25924 self.advance();
25925 let start = self.parse_frame_bound()?;
25926 if !matches!(self.peek(), Token::And) {
25927 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25928 }
25929 self.advance();
25930 let end = self.parse_frame_bound()?;
25931 (start, Some(end))
25932 } else {
25933 (self.parse_frame_bound()?, None)
25934 };
25935 let exclude = self.parse_frame_exclusion()?;
25936 Ok(WindowFrame {
25937 kind,
25938 start,
25939 end,
25940 exclude,
25941 })
25942 }
25943
25944 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25945 /// after a frame spec. NO OTHERS is the default no-op.
25946 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25947 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25948 return Ok(FrameExclusion::NoOthers);
25949 }
25950 self.advance(); // EXCLUDE
25951 match self.peek() {
25952 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25953 self.advance();
25954 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25955 return Err(self.err(format!(
25956 "expected ROW after EXCLUDE CURRENT, got {:?}",
25957 self.peek()
25958 )));
25959 }
25960 self.advance();
25961 Ok(FrameExclusion::CurrentRow)
25962 }
25963 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25964 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25965 // Without this arm it fell to the catch-all, whose message
25966 // self-contradictingly listed GROUP as expected.
25967 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25968 self.advance();
25969 Ok(FrameExclusion::Group)
25970 }
25971 Token::Group => {
25972 self.advance();
25973 Ok(FrameExclusion::Group)
25974 }
25975 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25976 self.advance();
25977 Ok(FrameExclusion::Ties)
25978 }
25979 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25980 self.advance();
25981 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25982 return Err(self.err(format!(
25983 "expected OTHERS after EXCLUDE NO, got {:?}",
25984 self.peek()
25985 )));
25986 }
25987 self.advance();
25988 Ok(FrameExclusion::NoOthers)
25989 }
25990 other => Err(self.err(format!(
25991 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25992 ))),
25993 }
25994 }
25995
25996 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25997 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25998 /// `UNBOUNDED FOLLOWING`.
25999 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26000 // Interval-typed offset for a value-based RANGE frame over a
26001 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26002 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26003 // PRECEDING`.
26004 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26005 let dir = self.expect_ident_like()?;
26006 return if dir.eq_ignore_ascii_case("preceding") {
26007 Ok(FrameBound::IntervalPreceding {
26008 months,
26009 days,
26010 micros,
26011 })
26012 } else if dir.eq_ignore_ascii_case("following") {
26013 Ok(FrameBound::IntervalFollowing {
26014 months,
26015 days,
26016 micros,
26017 })
26018 } else {
26019 Err(self.err(format!(
26020 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26021 )))
26022 };
26023 }
26024 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26025 if let Token::Integer(n) = *self.peek() {
26026 self.advance();
26027 let n: u64 = u64::try_from(n).map_err(|_| {
26028 self.err(format!(
26029 "invalid frame offset {n} — expected non-negative integer"
26030 ))
26031 })?;
26032 let dir = self.expect_ident_like()?;
26033 return if dir.eq_ignore_ascii_case("preceding") {
26034 Ok(FrameBound::OffsetPreceding(n))
26035 } else if dir.eq_ignore_ascii_case("following") {
26036 Ok(FrameBound::OffsetFollowing(n))
26037 } else {
26038 Err(self.err(format!(
26039 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26040 )))
26041 };
26042 }
26043 let first = self.expect_ident_like()?;
26044 if first.eq_ignore_ascii_case("unbounded") {
26045 let dir = self.expect_ident_like()?;
26046 return if dir.eq_ignore_ascii_case("preceding") {
26047 Ok(FrameBound::UnboundedPreceding)
26048 } else if dir.eq_ignore_ascii_case("following") {
26049 Ok(FrameBound::UnboundedFollowing)
26050 } else {
26051 Err(self.err(format!(
26052 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26053 )))
26054 };
26055 }
26056 if first.eq_ignore_ascii_case("current") {
26057 let row = self.expect_ident_like()?;
26058 if !row.eq_ignore_ascii_case("row") {
26059 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26060 }
26061 return Ok(FrameBound::CurrentRow);
26062 }
26063 Err(self.err(format!(
26064 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26065 )))
26066 }
26067
26068 /// Detect and consume a leading interval offset in a frame bound —
26069 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26070 /// `(months, days, micros)`. Leaves the cursor on the trailing
26071 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26072 /// when the next tokens are not an interval offset.
26073 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26074 // Shape A — `INTERVAL '1 day'`.
26075 if matches!(self.peek(), Token::Interval) {
26076 self.advance(); // INTERVAL
26077 let atom = self.parse_interval_atom()?;
26078 if let Expr::Literal(Literal::Interval {
26079 months,
26080 days,
26081 micros,
26082 ..
26083 }) = atom
26084 {
26085 return Ok(Some((months, days, micros)));
26086 }
26087 return Err(self.err("expected an interval literal in frame offset".to_string()));
26088 }
26089 // Shape B — `'1 day'::interval`. Look ahead for the exact
26090 // string / `::` / interval-target triple before committing.
26091 if let Token::String(text) = self.peek() {
26092 let target_is_interval = match self.tokens.get(self.pos + 2) {
26093 Some(Token::Interval) => true,
26094 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26095 _ => false,
26096 };
26097 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26098 && target_is_interval;
26099 if is_cast {
26100 let text = text.clone();
26101 self.advance(); // string
26102 self.advance(); // ::
26103 self.advance(); // interval
26104 let parts = parse_interval_text(&text).ok_or_else(|| {
26105 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26106 })?;
26107 return Ok(Some(parts));
26108 }
26109 }
26110 Ok(None)
26111 }
26112
26113 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26114 // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26115 // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26116 // and all three answer the literal on MySQL 9.7.2.
26117 //
26118 // It is not only syntax, which is why it waited for
26119 // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26120 // because `_binary` makes the comparison byte-wise, while
26121 // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26122 // dropping the charset would have turned a hard error into a
26123 // silently wrong comparison — worse than the error it replaced.
26124 //
26125 // An UNKNOWN charset is NOT an introducer: MySQL answers
26126 // `Unknown column '_nosuch'`, because it parses as a column
26127 // reference followed by a string. So the table decides, and it
26128 // is the same table `SET NAMES` reads.
26129 //
26130 // A space is allowed between the two (`_utf8mb4 'x'`), which
26131 // falls out of asking the token stream rather than the bytes.
26132 if self.mysql_dialect
26133 && let Token::String(_) = self.peek()
26134 {
26135 let lower = first.to_ascii_lowercase();
26136 let charset = if lower == "n" {
26137 // `N'…'` is the national character set, which MySQL
26138 // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26139 //
26140 // utf8mb3 and utf8mb4 both fold case in their default
26141 // collations, so nothing SPG can be asked distinguishes
26142 // the two here: an ablation swapping this to utf8mb4
26143 // reddens no pin. Recorded rather than implied — the
26144 // spelling follows MySQL's documentation, not a
26145 // measurement.
26146 Some("utf8mb3")
26147 } else {
26148 // No filter here: the lookup below IS the test for
26149 // "is this a charset". An ablation that removed a filter
26150 // in this spot reddened nothing, which is how the two
26151 // were found to be one check written twice.
26152 lower.strip_prefix('_')
26153 };
26154 if let Some(cs) = charset
26155 && let Some(collation) = crate::charset::charset_default_collation(cs)
26156 {
26157 let Token::String(body) = self.advance() else {
26158 unreachable!("peeked a string");
26159 };
26160 return Ok(Expr::Collate {
26161 expr: Box::new(Expr::Literal(Literal::String(body))),
26162 collation: String::from(collation),
26163 });
26164 }
26165 }
26166 if matches!(self.peek(), Token::Dot) {
26167 self.advance();
26168 let name = self.expect_ident_like()?;
26169 // v7.14.0 — schema-qualified function call
26170 // `<schema>.<fn>(args)`. PG dumps emit
26171 // `pg_catalog.set_config(...)` in the preamble. SPG
26172 // is single-namespace: drop the schema prefix and
26173 // route the dispatch on the bare function name.
26174 if matches!(self.peek(), Token::LParen) {
26175 return self.finish_ident_atom(name);
26176 }
26177 return Ok(Expr::Column(ColumnName {
26178 qualifier: Some(first),
26179 name,
26180 }));
26181 }
26182 if matches!(self.peek(), Token::LParen) {
26183 self.advance();
26184 // `COUNT(*)` — special-cased here because `*` isn't a normal
26185 // expression token. Lower-case match on `first` since the lexer
26186 // folds identifiers.
26187 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26188 self.advance();
26189 if !matches!(self.peek(), Token::RParen) {
26190 return Err(self.err(format!(
26191 "expected ')' after COUNT(*), got {:?}",
26192 self.peek()
26193 )));
26194 }
26195 self.advance();
26196 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26197 let filter = self.parse_filter_clause()?;
26198 // v4.12: COUNT(*) OVER (...) — same window tail.
26199 let null_treatment = self.parse_null_treatment_modifier();
26200 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26201 && s.eq_ignore_ascii_case("over")
26202 {
26203 self.advance();
26204 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26205 return Ok(Expr::WindowFunction {
26206 name: "count_star".into(),
26207 args: Vec::new(),
26208 partition_by,
26209 order_by,
26210 frame,
26211 null_treatment,
26212 filter,
26213 });
26214 }
26215 if let Some(filter) = filter {
26216 return Ok(Expr::AggregateOrdered {
26217 call: Box::new(Expr::FunctionCall {
26218 name: "count_star".into(),
26219 args: Vec::new(),
26220 }),
26221 order_by: Vec::new(),
26222 distinct: false,
26223 filter: Some(filter),
26224 });
26225 }
26226 return Ok(Expr::FunctionCall {
26227 name: "count_star".into(),
26228 args: Vec::new(),
26229 });
26230 }
26231 // Function call. PG-style: zero-or-more comma-separated args.
26232 let mut args = Vec::new();
26233 // v7.38 (read01, T14) — named-argument notation `argname => value`.
26234 // Names are collected in lock-step with `args` and resolved to
26235 // positional order after the loop (the AST stays positional).
26236 let mut arg_names: Vec<Option<String>> = Vec::new();
26237 let mut agg_order_by: Vec<OrderBy> = Vec::new();
26238 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26239 // seen, so the value arguments before it can be folded.
26240 let mut saw_separator = false;
26241 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26242 // v7.32 (round-29) — accept the dual `ALL` quantifier too
26243 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26244 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26245 self.advance();
26246 true
26247 } else if matches!(self.peek(), Token::All) {
26248 self.advance();
26249 false
26250 } else {
26251 false
26252 };
26253 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26254 // TIMESTAMPDIFF take a bare unit keyword as the first
26255 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26256 // bare type keyword (DATE / TIME / DATETIME); lower them
26257 // onto string literals so the evaluator sees plain text.
26258 if ((first.eq_ignore_ascii_case("timestampadd")
26259 || first.eq_ignore_ascii_case("timestampdiff"))
26260 && matches!(self.peek(), Token::Ident(u) if matches!(
26261 u.to_ascii_lowercase().as_str(),
26262 "microsecond" | "second" | "minute" | "hour" | "day"
26263 | "week" | "month" | "quarter" | "year"
26264 )))
26265 || (first.eq_ignore_ascii_case("get_format")
26266 && matches!(self.peek(), Token::Ident(u) if matches!(
26267 u.to_ascii_lowercase().as_str(),
26268 "date" | "time" | "datetime" | "timestamp"
26269 )))
26270 {
26271 if let Token::Ident(u) = self.peek() {
26272 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26273 }
26274 self.advance();
26275 if matches!(self.peek(), Token::Comma) {
26276 self.advance();
26277 }
26278 }
26279 // `ROW(a, b, …)` keyword constructor. Followed by a
26280 // comparison operator or [NOT] IN it joins the paren
26281 // row-constructor machinery (fieldwise parse-time
26282 // expansion); bare, it stays a `row` call the evaluator
26283 // renders as PG record text.
26284 if first.eq_ignore_ascii_case("row") {
26285 let mut row_items = Vec::new();
26286 if !matches!(self.peek(), Token::RParen) {
26287 loop {
26288 row_items.push(self.parse_expr(0)?);
26289 match self.peek() {
26290 Token::Comma => {
26291 self.advance();
26292 }
26293 Token::RParen => break,
26294 other => {
26295 return Err(self.err(format!(
26296 "expected ',' or ')' in ROW(...), got {other:?}"
26297 )));
26298 }
26299 }
26300 }
26301 }
26302 self.advance(); // ')'
26303 let comparison_follows = matches!(
26304 self.peek(),
26305 Token::Eq
26306 | Token::NotEq
26307 | Token::Lt
26308 | Token::LtEq
26309 | Token::Gt
26310 | Token::GtEq
26311 | Token::In
26312 ) || (matches!(self.peek(), Token::Not)
26313 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26314 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26315 if comparison_follows && !row_items.is_empty() {
26316 return self.parse_row_comparison_tail(row_items);
26317 }
26318 return Ok(Expr::FunctionCall {
26319 name: String::from("row"),
26320 args: row_items,
26321 });
26322 }
26323 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26324 // the parse-mode keyword introduces the source text. SPG
26325 // carries XML as text, so both modes lower to __xmlparse(expr)
26326 // which validates well-formedness and returns Value::Xml.
26327 if first.eq_ignore_ascii_case("xmlparse")
26328 && matches!(self.peek(), Token::Ident(kw)
26329 if kw.eq_ignore_ascii_case("document")
26330 || kw.eq_ignore_ascii_case("content"))
26331 {
26332 let mode = match self.advance() {
26333 Token::Ident(kw) => kw.to_ascii_lowercase(),
26334 _ => unreachable!("peeked an ident"),
26335 };
26336 let src = self.parse_expr(0)?;
26337 if !matches!(self.peek(), Token::RParen) {
26338 return Err(self.err(format!(
26339 "expected ')' to close XMLPARSE, got {:?}",
26340 self.peek()
26341 )));
26342 }
26343 self.advance();
26344 return Ok(Expr::FunctionCall {
26345 name: String::from("__xmlparse"),
26346 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26347 });
26348 }
26349 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26350 // keyword introduces the element name (a bare or quoted
26351 // identifier), then optional content expressions. Lower to a
26352 // plain `xmlelement(name_text, content …)` call.
26353 if first.eq_ignore_ascii_case("xmlelement")
26354 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26355 {
26356 self.advance(); // consume NAME
26357 let elem_name = match self.peek().clone() {
26358 Token::Ident(n) | Token::QuotedIdent(n) => {
26359 self.advance();
26360 n
26361 }
26362 other => {
26363 return Err(self.err(format!(
26364 "expected element name after XMLELEMENT NAME, got {other:?}"
26365 )));
26366 }
26367 };
26368 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26369 while matches!(self.peek(), Token::Comma) {
26370 self.advance();
26371 args.push(self.parse_expr(0)?);
26372 }
26373 if !matches!(self.peek(), Token::RParen) {
26374 return Err(self.err(format!(
26375 "expected ')' to close XMLELEMENT, got {:?}",
26376 self.peek()
26377 )));
26378 }
26379 self.advance();
26380 return Ok(Expr::FunctionCall {
26381 name: String::from("xmlelement"),
26382 args,
26383 });
26384 }
26385 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26386 // becomes a `<name>value</name>` element; a bare column infers its
26387 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26388 // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26389 // `CONVERT(expr USING cs)` was a syntax error at USING, and
26390 // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26391 // `convert(bytea, src, dest)` and answered `column "char" does
26392 // not exist`. Both are casts in MySQL: measured on 9.7.2,
26393 // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26394 // both 'A', and `CONVERT(123, CHAR)` is '123'.
26395 //
26396 // The charset is checked against the same table the introducers
26397 // use, so an unknown one is refused rather than quietly ignored.
26398 if self.mysql_dialect
26399 && first.eq_ignore_ascii_case("convert")
26400 && !matches!(self.peek(), Token::RParen)
26401 {
26402 let save = self.pos;
26403 let inner = self.parse_expr(0)?;
26404 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26405 self.advance();
26406 let cs = match self.peek().clone() {
26407 Token::Ident(n) | Token::QuotedIdent(n) => {
26408 self.advance();
26409 n
26410 }
26411 other => {
26412 return Err(self.err(alloc::format!(
26413 "expected a charset after USING, got {other:?}"
26414 )));
26415 }
26416 };
26417 let lc = cs.to_ascii_lowercase();
26418 if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26419 return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26420 }
26421 if !matches!(self.peek(), Token::RParen) {
26422 return Err(self.err(alloc::format!(
26423 "expected ')' after CONVERT … USING, got {:?}",
26424 self.peek()
26425 )));
26426 }
26427 self.advance();
26428 let target = if lc == "binary" {
26429 CastTarget::Named("binary".to_string())
26430 } else {
26431 CastTarget::Text
26432 };
26433 return self.finish_postfix_casts(Expr::Cast {
26434 expr: alloc::boxed::Box::new(inner),
26435 target,
26436 });
26437 }
26438 if matches!(self.peek(), Token::Comma) {
26439 self.advance();
26440 // A type name here is MySQL's cast form; anything else
26441 // (three string arguments) is PostgreSQL's `convert`,
26442 // which keeps its own path.
26443 if let Ok(target) = self.parse_cast_target()
26444 && matches!(self.peek(), Token::RParen)
26445 {
26446 self.advance();
26447 return self.finish_postfix_casts(Expr::Cast {
26448 expr: alloc::boxed::Box::new(inner),
26449 target,
26450 });
26451 }
26452 }
26453 self.pos = save;
26454 }
26455 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26456 let mut args: Vec<Expr> = Vec::new();
26457 loop {
26458 let val = self.parse_expr(0)?;
26459 let name = if matches!(self.peek(), Token::As) {
26460 self.advance();
26461 match self.peek().clone() {
26462 Token::Ident(n) | Token::QuotedIdent(n) => {
26463 self.advance();
26464 n
26465 }
26466 other => {
26467 return Err(self.err(format!(
26468 "expected name after AS in XMLFOREST, got {other:?}"
26469 )));
26470 }
26471 }
26472 } else if let Expr::Column(c) = &val {
26473 c.name.clone()
26474 } else {
26475 return Err(
26476 self.err("XMLFOREST element without a column name needs AS".into())
26477 );
26478 };
26479 args.push(Expr::Literal(Literal::String(name)));
26480 args.push(val);
26481 if matches!(self.peek(), Token::Comma) {
26482 self.advance();
26483 } else {
26484 break;
26485 }
26486 }
26487 if !matches!(self.peek(), Token::RParen) {
26488 return Err(self.err(format!(
26489 "expected ')' to close XMLFOREST, got {:?}",
26490 self.peek()
26491 )));
26492 }
26493 self.advance();
26494 return Ok(Expr::FunctionCall {
26495 name: String::from("xmlforest"),
26496 args,
26497 });
26498 }
26499 // SQL-standard `POSITION(sub IN str)` — lowers onto
26500 // strpos(str, sub). IN is the argument separator here,
26501 // so the needle parses with the IN-tail suppressed.
26502 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26503 let saved = self.suppress_in_tail;
26504 self.suppress_in_tail = true;
26505 let needle = self.parse_expr(0);
26506 self.suppress_in_tail = saved;
26507 let needle = needle?;
26508 if matches!(self.peek(), Token::In) {
26509 self.advance();
26510 let haystack = self.parse_expr(0)?;
26511 if !matches!(self.peek(), Token::RParen) {
26512 return Err(self.err(format!(
26513 "expected ')' to close POSITION, got {:?}",
26514 self.peek()
26515 )));
26516 }
26517 self.advance();
26518 return Ok(Expr::FunctionCall {
26519 name: String::from("strpos"),
26520 args: alloc::vec![haystack, needle],
26521 });
26522 }
26523 // position(sub, str) comma form (incl. bytea) —
26524 // hand the parsed first arg to the generic list.
26525 args.push(needle);
26526 if matches!(self.peek(), Token::Comma) {
26527 self.advance();
26528 }
26529 }
26530 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26531 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26532 // plain comma forms TRIM(str) / TRIM(str, chars) keep
26533 // riding the generic argument list below.
26534 if first.eq_ignore_ascii_case("trim") {
26535 let mode = match self.peek() {
26536 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26537 self.advance();
26538 Some("btrim")
26539 }
26540 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26541 self.advance();
26542 Some("ltrim")
26543 }
26544 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26545 self.advance();
26546 Some("rtrim")
26547 }
26548 _ => None,
26549 };
26550 if mode.is_some() || matches!(self.peek(), Token::From) {
26551 // TRIM([mode] FROM str) — no strip-chars.
26552 let chars = if matches!(self.peek(), Token::From) {
26553 None
26554 } else {
26555 Some(self.parse_expr(0)?)
26556 };
26557 if !matches!(self.peek(), Token::From) {
26558 return Err(self.err(format!(
26559 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26560 self.peek()
26561 )));
26562 }
26563 self.advance();
26564 let target = self.parse_expr(0)?;
26565 if !matches!(self.peek(), Token::RParen) {
26566 return Err(
26567 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26568 );
26569 }
26570 self.advance();
26571 let mut trim_args = alloc::vec![target];
26572 if let Some(c) = chars {
26573 trim_args.push(c);
26574 }
26575 return Ok(Expr::FunctionCall {
26576 name: String::from(mode.unwrap_or("btrim")),
26577 args: trim_args,
26578 });
26579 }
26580 }
26581 if !matches!(self.peek(), Token::RParen) {
26582 loop {
26583 // v7.38 (read01, T14) — `argname => value` names this arg.
26584 // v7.39 (read01 round 77) — `argname := value` is the same
26585 // thing, and it is the spelling PG's own docs lead with. It
26586 // was simply never lexed here, so every `f(x := 1)` died in
26587 // the parser regardless of what `f` was.
26588 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26589 (
26590 Token::Ident(n) | Token::QuotedIdent(n),
26591 Some(Token::FatArrow | Token::ColonEq),
26592 ) => {
26593 let name = n.clone();
26594 self.advance(); // name
26595 self.advance(); // => / :=
26596 Some(name)
26597 }
26598 _ => None,
26599 };
26600 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26601 // array's elements into a variadic call's trailing args
26602 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26603 // reserved, so it arrives as a bare ident before the arg.
26604 let is_variadic = this_name.is_none()
26605 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26606 if is_variadic {
26607 self.advance();
26608 }
26609 let arg = self.parse_expr(0)?;
26610 args.push(match &this_name {
26611 // The callee's parameter names decide the slot, and a
26612 // user function's live in the catalog. Carry the name
26613 // to eval rather than guessing here.
26614 Some(n) => Expr::NamedArg {
26615 name: n.clone(),
26616 expr: Box::new(arg),
26617 },
26618 None if is_variadic => Expr::Variadic(Box::new(arg)),
26619 None => arg,
26620 });
26621 arg_names.push(this_name);
26622 // v7.25 (round-17) — standard `CAST(expr AS type)`.
26623 // The `::` cast already worked; this lowers the
26624 // function form onto the same Expr::Cast node.
26625 if first.eq_ignore_ascii_case("cast")
26626 && args.len() == 1
26627 && matches!(self.peek(), Token::As)
26628 {
26629 self.advance();
26630 let target = self.parse_cast_target()?;
26631 if !matches!(self.peek(), Token::RParen) {
26632 return Err(self.err(format!(
26633 "expected ')' to close CAST, got {:?}",
26634 self.peek()
26635 )));
26636 }
26637 self.advance();
26638 return Ok(Expr::Cast {
26639 expr: Box::new(args.pop().expect("one arg")),
26640 target,
26641 });
26642 }
26643 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26644 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26645 // keywords; SPG's lexer makes them plain idents (so they'd be
26646 // read as column refs). Lower the keyword to the string form
26647 // the evaluator already accepts.
26648 if first.eq_ignore_ascii_case("normalize")
26649 && args.len() == 1
26650 && matches!(self.peek(), Token::Comma)
26651 {
26652 let form = match self.tokens.get(self.pos + 1) {
26653 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26654 let up = f.to_ascii_uppercase();
26655 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26656 }
26657 _ => None,
26658 };
26659 if let Some(up) = form {
26660 self.advance(); // comma
26661 self.advance(); // form keyword
26662 args.push(Expr::Literal(Literal::String(up)));
26663 }
26664 }
26665 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26666 // form. Desugars to the comma-list shape evaluator already
26667 // handles. Triggered after the first arg when the function
26668 // name is substring / substr and the next token is FROM
26669 // (a reserved keyword in PG; SPG also reserves it).
26670 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26671 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26672 // internal __substring_similar(str, pat, esc) call.
26673 if (first.eq_ignore_ascii_case("substring")
26674 || first.eq_ignore_ascii_case("substr"))
26675 && args.len() == 1
26676 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26677 {
26678 self.advance(); // SIMILAR
26679 let pattern = self.parse_expr(0)?;
26680 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26681 {
26682 return Err(self.err(format!(
26683 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26684 self.peek()
26685 )));
26686 }
26687 self.advance(); // ESCAPE
26688 let esc = self.parse_expr(0)?;
26689 if !matches!(self.peek(), Token::RParen) {
26690 return Err(self.err(format!(
26691 "expected ')' to close substring(... SIMILAR ...), got {:?}",
26692 self.peek()
26693 )));
26694 }
26695 self.advance();
26696 args.push(pattern);
26697 args.push(esc);
26698 return Ok(Expr::FunctionCall {
26699 name: "__substring_similar".to_string(),
26700 args,
26701 });
26702 }
26703 if (first.eq_ignore_ascii_case("substring")
26704 || first.eq_ignore_ascii_case("substr"))
26705 && args.len() == 1
26706 && matches!(self.peek(), Token::From | Token::For)
26707 {
26708 // `substring(str FROM pos [FOR len])`, or the FOR-only
26709 // `substring(str FOR len)` which PG treats as FROM 1.
26710 if matches!(self.peek(), Token::From) {
26711 self.advance();
26712 let start = self.parse_expr(0)?;
26713 args.push(start);
26714 } else {
26715 args.push(Expr::Literal(Literal::Integer(1)));
26716 }
26717 if matches!(self.peek(), Token::For) {
26718 self.advance();
26719 let length = self.parse_expr(0)?;
26720 args.push(length);
26721 }
26722 if !matches!(self.peek(), Token::RParen) {
26723 return Err(self.err(format!(
26724 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26725 self.peek()
26726 )));
26727 }
26728 self.advance();
26729 return Ok(Expr::FunctionCall {
26730 name: first.to_ascii_lowercase(),
26731 args,
26732 });
26733 }
26734 // PG `overlay(str PLACING repl FROM n [FOR len])`
26735 // syntactic form. Desugars to the `overlay(str,
26736 // repl, n[, len])` comma-list shape the evaluator
26737 // already implements. `PLACING` is not a reserved
26738 // token in SPG, so it arrives as a bare Ident.
26739 if first.eq_ignore_ascii_case("overlay")
26740 && args.len() == 1
26741 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26742 {
26743 self.advance(); // consume PLACING
26744 args.push(self.parse_expr(0)?); // replacement
26745 if !matches!(self.peek(), Token::From) {
26746 return Err(self.err(format!(
26747 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26748 self.peek()
26749 )));
26750 }
26751 self.advance();
26752 args.push(self.parse_expr(0)?); // start position
26753 if matches!(self.peek(), Token::For) {
26754 self.advance();
26755 args.push(self.parse_expr(0)?); // length
26756 }
26757 if !matches!(self.peek(), Token::RParen) {
26758 return Err(self.err(format!(
26759 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26760 self.peek()
26761 )));
26762 }
26763 self.advance();
26764 return Ok(Expr::FunctionCall {
26765 name: String::from("overlay"),
26766 args,
26767 });
26768 }
26769 // `TRIM(chars FROM str)` — the keyword-less
26770 // spelling lands here after the chars parse
26771 // (the keyword forms return earlier).
26772 if first.eq_ignore_ascii_case("trim")
26773 && args.len() == 1
26774 && matches!(self.peek(), Token::From)
26775 {
26776 self.advance();
26777 let target = self.parse_expr(0)?;
26778 if !matches!(self.peek(), Token::RParen) {
26779 return Err(self.err(format!(
26780 "expected ')' to close TRIM(chars FROM str), got {:?}",
26781 self.peek()
26782 )));
26783 }
26784 self.advance();
26785 let chars = args.pop().expect("one arg");
26786 return Ok(Expr::FunctionCall {
26787 name: String::from("btrim"),
26788 args: alloc::vec![target, chars],
26789 });
26790 }
26791 // v7.24 (round-16 A) — aggregate-internal
26792 // ordering: `array_agg(x ORDER BY y DESC NULLS
26793 // LAST)`. Keys close the argument list.
26794 if matches!(self.peek(), Token::Order) {
26795 self.advance();
26796 if !self.peek_is_by() {
26797 return Err(self.err(format!(
26798 "expected BY after ORDER in aggregate args, got {:?}",
26799 self.peek()
26800 )));
26801 }
26802 self.advance();
26803 loop {
26804 // v7.39 (round 691) — save/restore, the discipline this parser
26805 // already uses around `pending_sample_preds`, so a subquery inside
26806 // a key neither inherits nor leaks the channel.
26807 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26808 let saved_coll = self.order_key_collation.take();
26809 let parsed = self.parse_expr(0);
26810 self.in_order_by_key = saved_flag;
26811 let collation =
26812 core::mem::replace(&mut self.order_key_collation, saved_coll);
26813 let expr = parsed?;
26814 let desc = if matches!(self.peek(), Token::Desc) {
26815 self.advance();
26816 true
26817 } else if matches!(self.peek(), Token::Asc) {
26818 self.advance();
26819 false
26820 } else {
26821 false
26822 };
26823 let nulls_first = self.parse_optional_nulls_placement()?;
26824 agg_order_by.push(OrderBy {
26825 expr,
26826 desc,
26827 nulls_first,
26828 collation,
26829 });
26830 if matches!(self.peek(), Token::Comma) {
26831 self.advance();
26832 } else {
26833 break;
26834 }
26835 }
26836 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26837 // follow the ORDER BY inside GROUP_CONCAT.
26838 if self.consume_group_concat_separator(&mut args)? {
26839 saw_separator = true;
26840 }
26841 if !matches!(self.peek(), Token::RParen) {
26842 return Err(self.err(format!(
26843 "expected ')' after aggregate ORDER BY, got {:?}",
26844 self.peek()
26845 )));
26846 }
26847 break;
26848 }
26849 // v7.39 (round 354, M12) — …or directly after the
26850 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26851 // own spelling of what PG passes as string_agg's second
26852 // argument; it was a parse error, so every MySQL query
26853 // that names its own separator failed outright.
26854 if self.consume_group_concat_separator(&mut args)? {
26855 saw_separator = true;
26856 break;
26857 }
26858 match self.peek() {
26859 Token::Comma => {
26860 self.advance();
26861 }
26862 Token::RParen => break,
26863 other => {
26864 return Err(self.err(format!(
26865 "expected ',' or ')' in function args, got {other:?}"
26866 )));
26867 }
26868 }
26869 }
26870 }
26871 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26872 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26873 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26874 // meaning a separator — that is what the explicit SEPARATOR
26875 // tail is for. Fold them into one `concat(...)` so the
26876 // aggregate keeps its single value argument.
26877 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26878 let values = args.len() - usize::from(saw_separator);
26879 if values > 1 {
26880 let sep_arg = if saw_separator { args.pop() } else { None };
26881 let folded = Expr::FunctionCall {
26882 name: "concat".to_string(),
26883 args: core::mem::take(&mut args),
26884 };
26885 args.push(folded);
26886 if let Some(sep) = sep_arg {
26887 args.push(sep);
26888 }
26889 }
26890 }
26891 self.advance(); // consume ')'
26892 // v7.39 (read01 round 77) — named arguments are NOT reordered here
26893 // any more. The parser has no catalog, so it could only ever resolve
26894 // the handful of `make_*` builtins whose parameter names were baked
26895 // into a table right here — every user function got
26896 // "does not support named arguments", though the catalog has been
26897 // storing its parameter names all along. Reordering happens in eval,
26898 // in one place, for builtins and user functions alike.
26899 // v7.32 (round-29) — ordered-set aggregate tail
26900 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26901 // (percentile_cont / percentile_disc / mode). The sort spec
26902 // lands in the same `order_by` slot a decorated aggregate
26903 // uses; the executor dispatches on the function name. WITHIN
26904 // GROUP and an intra-argument ORDER BY are mutually
26905 // exclusive (PG rejects both).
26906 let within_group_order = self.parse_within_group_clause()?;
26907 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26908 return Err(self.err(
26909 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26910 .into(),
26911 ));
26912 }
26913 let within_group_seen = !within_group_order.is_empty();
26914 let agg_order_by = if within_group_order.is_empty() {
26915 agg_order_by
26916 } else {
26917 within_group_order
26918 };
26919 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26920 let filter = self.parse_filter_clause()?;
26921 // v4.12: window-function tail — `name(args) OVER (...)`.
26922 // Promotes the just-parsed FunctionCall into a
26923 // WindowFunction node carrying partition + order.
26924 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26925 // / `RESPECT NULLS OVER (...)` between the closing paren
26926 // and `OVER`.
26927 let null_treatment = self.parse_null_treatment_modifier();
26928 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26929 && s.eq_ignore_ascii_case("over")
26930 {
26931 self.advance();
26932 // v7.39 (round 230) — PG implements neither modifier for a
26933 // windowed call and says so (0A000). Both used to be parsed
26934 // and then silently dropped here, so `count(DISTINCT v)
26935 // OVER (…)` quietly answered the non-distinct count.
26936 if agg_distinct {
26937 return Err(
26938 self.err("DISTINCT is not implemented for window functions".to_string())
26939 );
26940 }
26941 if !agg_order_by.is_empty() {
26942 // PG separates the two shapes that land here: a
26943 // WITHIN GROUP call is an ordered-set aggregate and gets
26944 // its own message naming the aggregate; a plain
26945 // `agg(x ORDER BY y)` gets the generic one.
26946 let msg = if within_group_seen {
26947 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26948 } else {
26949 "aggregate ORDER BY is not implemented for window functions".to_string()
26950 };
26951 return Err(self.err(msg));
26952 }
26953 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26954 return Ok(Expr::WindowFunction {
26955 name: first,
26956 args,
26957 partition_by,
26958 order_by,
26959 frame,
26960 null_treatment,
26961 filter,
26962 });
26963 }
26964 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26965 return Ok(Expr::AggregateOrdered {
26966 call: Box::new(Expr::FunctionCall { name: first, args }),
26967 order_by: agg_order_by,
26968 distinct: agg_distinct,
26969 filter,
26970 });
26971 }
26972 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26973 // over TIMESTAMPTZ and has no timestamp overload, so a
26974 // timestamp argument is coerced on the way in and the answer
26975 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26976 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26977 // zone`. SPG answered `timestamp without time zone`, dropping
26978 // the offset from every rendering.
26979 //
26980 // Writing the coercion PG performs makes the existing
26981 // argument-driven typing (the one `date_trunc` uses) reach the
26982 // right answer, rather than teaching the type layer a second
26983 // rule. MySQL's DATE_ADD is a different function that returns
26984 // DATE or DATETIME, so this is PG-dialect only.
26985 //
26986 // Out-of-line because this sits on the RECURSIVE descent
26987 // frame: an inline block with locals here costs every nesting
26988 // level, and the suite's deep-nesting sentinel overflowed the
26989 // 512 KiB parser stack the moment one was added (round 430's
26990 // lesson, in the same shape).
26991 if !self.mysql_dialect {
26992 lift_date_add_arg_to_timestamptz(&first, &mut args);
26993 }
26994 return Ok(Expr::FunctionCall { name: first, args });
26995 }
26996 // v7.9.20 — SQL-standard parenless keyword expressions
26997 // (PG treats these as functions called without parens).
26998 // Resolve to a synthetic FunctionCall so the engine's
26999 // eval path reuses the existing function-call routing.
27000 // mailrs G3.
27001 let lc = first.to_ascii_lowercase();
27002 if matches!(
27003 lc.as_str(),
27004 "current_date"
27005 | "current_time"
27006 | "current_timestamp"
27007 | "localtimestamp"
27008 | "localtime"
27009 // v7.37.17 (17.6 siblings) — session-identity SQL-
27010 // standard parenless keywords. current_user /
27011 // session_user / user were already caught by the
27012 // pgwire canned-response shortcut but bare-select
27013 // in the embedded engine went through Expr::Column
27014 // and errored. Adding them here so the parser
27015 // resolves to a synthetic FunctionCall that reuses
27016 // the existing eval/functions.rs dispatch.
27017 | "current_user"
27018 | "session_user"
27019 | "current_role"
27020 | "current_catalog"
27021 | "current_schema"
27022 | "current_database"
27023 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27024 | "system_user"
27025 ) {
27026 return Ok(Expr::FunctionCall {
27027 name: lc,
27028 args: Vec::new(),
27029 });
27030 }
27031 Ok(Expr::Column(ColumnName {
27032 qualifier: None,
27033 name: first,
27034 }))
27035 }
27036}
27037
27038/// v7.39 (round 522) — write the coercion PG's `date_add` /
27039/// `date_subtract` signature performs.
27040///
27041/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27042/// timestamp argument is cast on the way in and the answer is
27043/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27044/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27045/// `timestamp without time zone`, dropping the offset from every
27046/// rendering of the result.
27047///
27048/// Writing the cast the signature implies lets the existing
27049/// argument-driven typing (the one `date_trunc` uses) reach the right
27050/// answer instead of teaching the type layer a second rule. MySQL's
27051/// DATE_ADD is a different function returning DATE or DATETIME, so the
27052/// caller applies this in PG dialect only.
27053///
27054/// A free function, and not a block at the call site, because the caller
27055/// is on the recursive-descent frame chain.
27056#[inline(never)]
27057fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27058 if args.len() != 2
27059 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27060 {
27061 return;
27062 }
27063 let base = args.remove(0);
27064 args.insert(
27065 0,
27066 Expr::Cast {
27067 expr: Box::new(base),
27068 target: CastTarget::Timestamptz,
27069 },
27070 );
27071}
27072
27073/// v6.8.2 — walk an expression tree and return the first column
27074/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27075/// to derive `CreateIndexStatement.column` from an expression
27076/// key (so downstream planner code resolving a primary column
27077/// position keeps working with expression indexes). Returns
27078/// `None` when the expression has no column ref at all — caller
27079/// surfaces that as a parse error.
27080fn extract_first_column(expr: &Expr) -> Option<String> {
27081 match expr {
27082 Expr::Column(cn) => Some(cn.name.clone()),
27083 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27084 Expr::Binary { lhs, rhs, .. } => {
27085 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27086 }
27087 Expr::Unary { expr: e, .. } => extract_first_column(e),
27088 // v7.39 (read01 round 93) — a cast wraps its operand: a common
27089 // expression-index key is `lower(col::text)`, where the column
27090 // sits under the `::text` cast inside the function arg. Without
27091 // descending here the key was rejected as "references no column".
27092 Expr::Cast { expr: e, .. } => extract_first_column(e),
27093 // v7.39.2 — and a COLLATE wraps its operand the same way.
27094 // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27095 // column the moment the clause became a node instead of being
27096 // absorbed, and the key was rejected as referencing none. This
27097 // is the shape the wildcard below silently produces, which is
27098 // why it is spelled out.
27099 Expr::Collate { expr: e, .. } => extract_first_column(e),
27100 _ => None,
27101 }
27102}
27103
27104fn maybe_not(expr: Expr, negated: bool) -> Expr {
27105 if negated {
27106 Expr::Unary {
27107 op: UnOp::Not,
27108 expr: Box::new(expr),
27109 }
27110 } else {
27111 expr
27112 }
27113}
27114
27115/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27116/// things in the two dialects, and SPG read all three PG's way:
27117///
27118/// | token | PG (and SPG) | MySQL, measured |
27119/// |---|---|---|
27120/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27121/// | `&&` | inet / array overlap | **AND** |
27122/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27123///
27124/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27125/// answer with no error, which is why they are routed here rather than
27126/// left to the shared table.
27127impl Parser {
27128 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27129 if self.mysql_dialect {
27130 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27131 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27132 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27133 if let Token::Ident(w) = tok
27134 && w.eq_ignore_ascii_case("div")
27135 {
27136 return Some((BinOp::IntDiv, 8));
27137 }
27138 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27139 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27140 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27141 // there sits in operand position, not infix).
27142 if let Token::Ident(w) = tok
27143 && w.eq_ignore_ascii_case("mod")
27144 {
27145 return Some((BinOp::Mod, 8));
27146 }
27147 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27148 // plain ident to the lexer. Its precedence sits between OR (1)
27149 // and AND (3) — hence rung 2, the slot freed by moving AND up.
27150 if let Token::Ident(w) = tok
27151 && w.eq_ignore_ascii_case("xor")
27152 {
27153 return Some((BinOp::LogicalXor, 2));
27154 }
27155 match tok {
27156 Token::Concat => return Some((BinOp::Or, 1)),
27157 // MySQL's `&&` is logical AND, sharing AND's rung (3).
27158 Token::InetOverlap => return Some((BinOp::And, 3)),
27159 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27160 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27161 _ => {}
27162 }
27163 }
27164 binop_from(tok)
27165 }
27166}
27167
27168// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27169// (which sits strictly between OR and AND), every level from AND upward was
27170// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27171// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27172// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27173// the *relative* order of every PG operator is unchanged by the shift.
27174fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27175 let pair = match tok {
27176 Token::Or => (BinOp::Or, 1),
27177 Token::And => (BinOp::And, 3),
27178 Token::Eq => (BinOp::Eq, 5),
27179 Token::NotEq => (BinOp::NotEq, 5),
27180 Token::Lt => (BinOp::Lt, 5),
27181 Token::LtEq => (BinOp::LtEq, 5),
27182 Token::Gt => (BinOp::Gt, 5),
27183 Token::GtEq => (BinOp::GtEq, 5),
27184 // pgvector distance ops all sit on the same rung — tighter than
27185 // comparisons (5) so `col <-> v < threshold` parses correctly.
27186 Token::L2Distance => (BinOp::L2Distance, 6),
27187 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27188 // comparison rung.
27189 Token::GeomParallel => (BinOp::GeomParallel, 5),
27190 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27191 // comparison rung.
27192 Token::OverLeft => (BinOp::OverLeft, 5),
27193 Token::OverRight => (BinOp::OverRight, 5),
27194 Token::GeomPerp => (BinOp::GeomPerp, 5),
27195 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27196 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27197 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27198 Token::InnerProduct => (BinOp::InnerProduct, 6),
27199 Token::CosineDistance => (BinOp::CosineDistance, 6),
27200 Token::Plus => (BinOp::Add, 7),
27201 Token::Minus => (BinOp::Sub, 7),
27202 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27203 // binds every "other" operator (`||`, `|`, `&`, `#`, the
27204 // pgvector distances above) BETWEEN additive (7) and the
27205 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27206 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27207 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27208 // ("matches PG conceptually" — the round-753 audit measured it
27209 // false; the old rung errored on `'a' || 1 + 1` with
27210 // `text + integer`). Same-level chains left-fold, as PG does.
27211 Token::Concat => (BinOp::Concat, 6),
27212 Token::Pipe => (BinOp::BitOr, 6),
27213 Token::Amp => (BinOp::BitAnd, 6),
27214 Token::Star => (BinOp::Mul, 8),
27215 Token::Slash => (BinOp::Div, 8),
27216 Token::Percent => (BinOp::Mod, 8),
27217 // v4.14: JSON path ops bind tighter than comparisons (5)
27218 // and additive (7) so `doc->'k' = 'v'` parses correctly.
27219 // Same rung as the multiplicative ops.
27220 Token::JsonGet => (BinOp::JsonGet, 8),
27221 Token::JsonGetText => (BinOp::JsonGetText, 8),
27222 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27223 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27224 Token::JsonContains => (BinOp::JsonContains, 8),
27225 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27226 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27227 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27228 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27229 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27230 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27231 // v7.12.2 — `@@` binds at the comparison rung (looser than
27232 // arithmetic, tighter than AND / OR). PG places `@@` at
27233 // the same precedence as `=` / `<`, so we follow.
27234 Token::TsMatch => (BinOp::TsMatch, 5),
27235 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27236 // PG places these at the comparison rung (same level as `=`),
27237 // so we follow.
27238 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27239 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27240 Token::InetContains => (BinOp::InetContains, 5),
27241 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27242 Token::InetOverlap => (BinOp::InetOverlap, 5),
27243 // v7.39 (round 508) — the geometric and pattern-order predicates
27244 // ride the comparison rung, as every other predicate does.
27245 Token::Intersects => (BinOp::Intersects, 5),
27246 Token::IsBelow => (BinOp::IsBelow, 5),
27247 Token::IsAbove => (BinOp::IsAbove, 5),
27248 Token::PatternLt => (BinOp::PatternLt, 5),
27249 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27250 Token::PatternGt => (BinOp::PatternGt, 5),
27251 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27252 // `@@@` is the old spelling of `@@` and means exactly it.
27253 Token::TsMatchOld => (BinOp::TsMatch, 5),
27254 _ => return None,
27255 };
27256 Some(pair)
27257}
27258
27259#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27260// `as f32` here is intentional: vector elements widen / narrow into f32 on
27261// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27262// past ~15 decimal digits — both are acceptable for a fixed-precision
27263// pgvector column.
27264/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27265/// implicit table alias and break trailing clauses. WITH lands
27266/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27267/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27268/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27269/// / VALUES / FOR / LATERAL — all of which would otherwise be
27270/// silently swallowed by `parse_optional_alias`.
27271fn is_alias_stopword(s: &str) -> bool {
27272 matches!(
27273 s.to_ascii_lowercase().as_str(),
27274 "with"
27275 | "on"
27276 | "where"
27277 | "having"
27278 | "group"
27279 | "order"
27280 | "limit"
27281 | "offset"
27282 | "union"
27283 | "except"
27284 | "intersect"
27285 | "returning"
27286 | "set"
27287 | "values"
27288 | "for"
27289 | "window"
27290 | "tablesample"
27291 | "lateral"
27292 | "left"
27293 | "right"
27294 | "inner"
27295 | "outer"
27296 | "full"
27297 | "cross"
27298 | "join"
27299 | "natural"
27300 | "using"
27301 | "fetch"
27302 )
27303}
27304
27305fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27306 match e {
27307 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27308 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27309 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27310 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27311 // so scale the divisor by hand instead of `f32::powi`.)
27312 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27313 let mut div = 1.0f32;
27314 for _ in 0..*scale {
27315 div *= 10.0;
27316 }
27317 Some(*unscaled as f32 / div)
27318 }
27319 Expr::Unary {
27320 op: UnOp::Neg,
27321 expr,
27322 } => extract_numeric_literal(expr).map(|x| -x),
27323 _ => None,
27324 }
27325}
27326
27327/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27328/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27329/// negative. Returns `None` if any pair fails to parse or no pair is found.
27330///
27331/// Recognised units (case-insensitive, optional trailing `s`):
27332/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27333/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27334/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27335/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27336/// (PG-canonical: DST and month-boundary semantics depend on this).
27337/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27338/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27339/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27340#[allow(clippy::cast_possible_truncation)]
27341fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27342 let mut months: i64 = 0;
27343 let mut days: i64 = 0;
27344 let mut micros: i64 = 0;
27345 let mut in_time = false;
27346 let mut num = alloc::string::String::new();
27347 for ch in rest.chars() {
27348 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27349 num.push(ch);
27350 continue;
27351 }
27352 if ch == 'T' || ch == 't' {
27353 if !num.is_empty() {
27354 return None;
27355 }
27356 in_time = true;
27357 continue;
27358 }
27359 let n: f64 = num.parse().ok()?;
27360 num.clear();
27361 match (ch, in_time) {
27362 ('Y' | 'y', false) => months += (n * 12.0) as i64,
27363 ('M', false) => months += n as i64,
27364 ('W' | 'w', false) => days += (n * 7.0) as i64,
27365 ('D' | 'd', false) => days += n as i64,
27366 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27367 ('M', true) => micros += (n * 60_000_000.0) as i64,
27368 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27369 _ => return None,
27370 }
27371 }
27372 if !num.is_empty() {
27373 return None;
27374 }
27375 Some((
27376 i32::try_from(months).ok()?,
27377 i32::try_from(days).ok()?,
27378 micros,
27379 ))
27380}
27381
27382/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27383/// leading `-` negates the whole value). Rejects date-like strings.
27384fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27385 let (neg, body) = match s.strip_prefix('-') {
27386 Some(b) => (true, b),
27387 None => (false, s),
27388 };
27389 let (y, m) = body.split_once('-')?;
27390 let years: i32 = y.parse().ok()?;
27391 let mons: i32 = m.parse().ok()?;
27392 if years < 0 || mons < 0 {
27393 return None;
27394 }
27395 let total = years.checked_mul(12)?.checked_add(mons)?;
27396 Some((if neg { -total } else { total }, 0, 0))
27397}
27398
27399/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27400/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27401fn parse_interval_clock(tok: &str) -> Option<i64> {
27402 let (neg, body) = match tok.strip_prefix('-') {
27403 Some(r) => (true, r),
27404 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27405 };
27406 let mut it = body.split(':');
27407 let h: i64 = it.next()?.parse().ok()?;
27408 let m: i64 = it.next()?.parse().ok()?;
27409 let s_tok = it.next().unwrap_or("0");
27410 if it.next().is_some() {
27411 return None;
27412 }
27413 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27414 let sec: i64 = sec.parse().ok()?;
27415 let mut f = alloc::string::String::from(frac);
27416 while f.len() < 6 {
27417 f.push('0');
27418 }
27419 f.truncate(6);
27420 let fus: i64 = f.parse().ok()?;
27421 sec.checked_mul(1_000_000)?.checked_add(fus)?
27422 } else {
27423 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27424 };
27425 let total = h
27426 .checked_mul(3_600_000_000)?
27427 .checked_add(m.checked_mul(60_000_000)?)?
27428 .checked_add(sec_us)?;
27429 Some(if neg { -total } else { total })
27430}
27431
27432/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27433/// every spelling PG accepts (measured against live PG18.4, not guessed):
27434/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27435/// Before this, the unit table matched long names only, with an ad-hoc
27436/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27437/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27438/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27439/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27440/// fractional) both read from this one table now.
27441fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27442 let u = raw.to_ascii_lowercase();
27443 Some(match u.as_str() {
27444 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27445 "microsecond"
27446 }
27447 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27448 "millisecond"
27449 }
27450 "second" | "seconds" | "sec" | "secs" | "s" => "second",
27451 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27452 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27453 "day" | "days" | "d" => "day",
27454 "week" | "weeks" | "w" => "week",
27455 "month" | "months" | "mon" | "mons" => "month",
27456 "year" | "years" | "yr" | "yrs" | "y" => "year",
27457 "decade" | "decades" | "dec" | "decs" => "decade",
27458 "century" | "centuries" | "cent" | "c" => "century",
27459 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27460 _ => return None,
27461 })
27462}
27463
27464/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27465/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27467pub(crate) enum IntervalField {
27468 Year,
27469 Month,
27470 Day,
27471 Hour,
27472 Minute,
27473 Second,
27474}
27475
27476/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27477/// spellings aren't standard for the qualifier position, so only the singular
27478/// forms are accepted.
27479/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27480/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27481/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27482/// take a `'1 2'` style literal — are not read here; they stay a parse
27483/// error rather than being silently misread.)
27484/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27485///
27486/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27487/// to do with a `@@` engine setting, and an unset one reads NULL rather
27488/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27489/// were the same node and `SELECT @x` answered "Unknown system variable".)
27490/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27491/// not see a session override — measured, after `SET autocommit=0`,
27492/// `@@global.autocommit` is still 1.
27493///
27494/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27495/// the parser's nesting budget is tuned against, and building these
27496/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27497/// wall `parse_left_right_atom` and friends were factored out for).
27498#[inline(never)]
27499fn variable_ref_atom(raw: &str) -> Expr {
27500 let user_var = !raw.starts_with("@@");
27501 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27502 Expr::FunctionCall {
27503 name: String::from(if user_var {
27504 "__spg_user_var"
27505 } else {
27506 "__spg_session_var"
27507 }),
27508 args: alloc::vec![Expr::Literal(Literal::String(bare))],
27509 }
27510}
27511
27512fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27513 let Token::Ident(s) = tok else { return None };
27514 Some(match () {
27515 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27516 () if s.eq_ignore_ascii_case("second") => "second",
27517 () if s.eq_ignore_ascii_case("minute") => "minute",
27518 () if s.eq_ignore_ascii_case("hour") => "hour",
27519 () if s.eq_ignore_ascii_case("day") => "day",
27520 () if s.eq_ignore_ascii_case("week") => "week",
27521 () if s.eq_ignore_ascii_case("month") => "month",
27522 () if s.eq_ignore_ascii_case("quarter") => "quarter",
27523 () if s.eq_ignore_ascii_case("year") => "year",
27524 () => return None,
27525 })
27526}
27527
27528/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27529/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27530/// which constructs the value at run time. Only the slot the unit names
27531/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27532/// slot the builtin has (months and fractional seconds respectively).
27533fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27534 let zero = || Expr::Literal(Literal::Integer(0));
27535 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27536 lhs: alloc::boxed::Box::new(qty.clone()),
27537 op,
27538 rhs: alloc::boxed::Box::new(by),
27539 };
27540 // (years, months, weeks, days, hours, mins, secs)
27541 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27542 match unit {
27543 "year" => args[0] = qty,
27544 "quarter" => {
27545 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27546 }
27547 "month" => args[1] = qty,
27548 "week" => args[2] = qty,
27549 "day" => args[3] = qty,
27550 "hour" => args[4] = qty,
27551 "minute" => args[5] = qty,
27552 "second" => args[6] = qty,
27553 // The builtin's seconds slot takes a fraction, so microseconds ride
27554 // it scaled down; the divisor is a NUMERIC literal so the division
27555 // stays exact rather than going through a float.
27556 "microsecond" => {
27557 args[6] = scaled(
27558 crate::ast::BinOp::Div,
27559 Expr::Literal(Literal::Numeric {
27560 unscaled: 1_000_000,
27561 scale: 0,
27562 }),
27563 );
27564 }
27565 _ => args[3] = qty,
27566 }
27567 Expr::FunctionCall {
27568 name: alloc::string::String::from("make_interval"),
27569 args,
27570 }
27571}
27572
27573/// `(count, unit)` → `(months, days, micros)`.
27574fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27575 let n: i64 = count.trim().parse().ok()?;
27576 Some(match unit {
27577 "microsecond" => (0, 0, n),
27578 "second" => (0, 0, n.checked_mul(1_000_000)?),
27579 "minute" => (0, 0, n.checked_mul(60_000_000)?),
27580 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27581 "day" => (0, i32::try_from(n).ok()?, 0),
27582 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27583 "month" => (i32::try_from(n).ok()?, 0, 0),
27584 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27585 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27586 _ => return None,
27587 })
27588}
27589
27590fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27591 let Token::Ident(s) = tok else { return None };
27592 Some(match () {
27593 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27594 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27595 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27596 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27597 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27598 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27599 () => return None,
27600 })
27601}
27602
27603/// v7.39 (read01 round 102) — interpret an interval literal under a field
27604/// qualifier. Returns `(months, days, micros)`.
27605///
27606/// * A single field applied to a bare number sets which unit the number means,
27607/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27608/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27609/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27610/// * Every other range, and any literal a single field can't read as a plain
27611/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27612/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27613/// like PG, and the qualifier there only bounds precision.
27614fn interpret_qualified_interval(
27615 text: &str,
27616 (f1, f2): (IntervalField, Option<IntervalField>),
27617) -> Option<(i32, i32, i64)> {
27618 if let Some(f2) = f2 {
27619 if f1 == IntervalField::Year && f2 == IntervalField::Month {
27620 if let Some(m) = parse_year_month_literal(text) {
27621 return Some((m, 0, 0));
27622 }
27623 }
27624 return parse_interval_text(text);
27625 }
27626 // Single field: reinterpret a bare number; otherwise the default parse.
27627 let trimmed = text.trim();
27628 if let Ok(val) = trimmed.parse::<f64>() {
27629 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27630 #[allow(clippy::cast_possible_truncation)]
27631 let whole = val as i64;
27632 #[allow(clippy::cast_possible_truncation)]
27633 let secs_micros = {
27634 let m = val * 1_000_000.0;
27635 if m >= 0.0 {
27636 (m + 0.5) as i64
27637 } else {
27638 (m - 0.5) as i64
27639 }
27640 };
27641 return Some(match f1 {
27642 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27643 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27644 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27645 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27646 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27647 IntervalField::Second => (0, 0, secs_micros),
27648 });
27649 }
27650 parse_interval_text(text)
27651}
27652
27653/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27654fn parse_year_month_literal(text: &str) -> Option<i32> {
27655 let t = text.trim();
27656 let (neg, body) = match t.strip_prefix('-') {
27657 Some(r) => (true, r),
27658 None => (false, t.strip_prefix('+').unwrap_or(t)),
27659 };
27660 let mut it = body.split('-');
27661 let years: i32 = it.next()?.trim().parse().ok()?;
27662 let months: i32 = match it.next() {
27663 Some(m) => m.trim().parse().ok()?,
27664 None => 0,
27665 };
27666 if it.next().is_some() {
27667 return None;
27668 }
27669 let total = years.checked_mul(12)?.checked_add(months)?;
27670 Some(if neg { -total } else { total })
27671}
27672
27673pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27674 // v7.38.19 — the two infinities, answered as the three extreme
27675 // fields PostgreSQL itself puts on the wire for them:
27676 //
27677 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27678 // … 7fffffffffffffff 7fffffff 7fffffff
27679 //
27680 // So no caller has to know the spelling — every one of them already
27681 // reads the three numbers, and `IntervalKind::from_fields` names
27682 // what they mean.
27683 //
27684 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27685 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27686 // infinity. Interval takes the full word, in any case.
27687 {
27688 let word = s.trim();
27689 let word = word.strip_prefix('@').map_or(word, str::trim);
27690 let (neg, body) = match word.strip_prefix('-') {
27691 Some(rest) => (true, rest.trim_start()),
27692 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27693 };
27694 if body.eq_ignore_ascii_case("infinity") {
27695 return Some(if neg {
27696 (i32::MIN, i32::MIN, i64::MIN)
27697 } else {
27698 (i32::MAX, i32::MAX, i64::MAX)
27699 });
27700 }
27701 }
27702 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27703 // `@` is decorative; a trailing `ago` negates the whole interval.
27704 let mut trimmed = s.trim();
27705 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27706 let mut negate = false;
27707 if let Some(rest) = trimmed
27708 .strip_suffix("ago")
27709 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27710 {
27711 negate = true;
27712 trimmed = rest.trim();
27713 }
27714 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27715 let (mo, d, us) = v?;
27716 if negate {
27717 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27718 } else {
27719 Some((mo, d, us))
27720 }
27721 };
27722 let s = trimmed;
27723 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27724 // are single tokens, not the `<n> <unit>` pair form handled below.
27725 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27726 return finish(parse_iso8601_interval(rest));
27727 }
27728 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27729 if let Some(iv) = parse_year_month_interval(trimmed) {
27730 return finish(Some(iv));
27731 }
27732 }
27733 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27734 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27735 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27736 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27737 if let Ok(n) = trimmed.parse::<i64>() {
27738 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27739 }
27740 if let Ok(f) = trimmed.parse::<f64>() {
27741 if f.is_finite() {
27742 #[allow(clippy::cast_possible_truncation)]
27743 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27744 }
27745 }
27746 }
27747 // v7.39 (round 243) — PG accepts the number and unit run together
27748 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27749 // the `<n> <unit>` pair loop below sees them as two.
27750 let raw_parts: Vec<&str> = s.split_whitespace().collect();
27751 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27752 for p in raw_parts {
27753 let boundary = p
27754 .char_indices()
27755 .find(|(i, c)| {
27756 *i > 0
27757 && c.is_ascii_alphabetic()
27758 && p[..*i]
27759 .chars()
27760 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27761 && p[..*i].chars().any(|d| d.is_ascii_digit())
27762 })
27763 .map(|(i, _)| i);
27764 match boundary {
27765 Some(i) => {
27766 parts.push(&p[..i]);
27767 parts.push(&p[i..]);
27768 }
27769 None => parts.push(p),
27770 }
27771 }
27772 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27773 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27774 // remains is the `<n> <unit>` pair form handled below.
27775 let mut clock_us: i64 = 0;
27776 let mut had_clock = false;
27777 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27778 clock_us = parse_interval_clock(parts[pos])?;
27779 parts.remove(pos);
27780 had_clock = true;
27781 }
27782 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27783 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27784 let mut lone_days: i32 = 0;
27785 if had_clock && parts.len() == 1 {
27786 if let Ok(n) = parts[0].parse::<i64>() {
27787 lone_days = i32::try_from(n).ok()?;
27788 parts.clear();
27789 }
27790 }
27791 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27792 return None;
27793 }
27794 let mut months: i32 = 0;
27795 let mut days: i32 = lone_days;
27796 let mut micros: i64 = clock_us;
27797 let mut i = 0;
27798 while i < parts.len() {
27799 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27800 if let Ok(n) = parts[i].parse::<i64>() {
27801 match unit_stripped {
27802 "microsecond" => micros = micros.checked_add(n)?,
27803 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27804 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27805 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27806 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27807 "day" => {
27808 let n32 = i32::try_from(n).ok()?;
27809 days = days.checked_add(n32)?;
27810 }
27811 "week" => {
27812 let n32 = i32::try_from(n).ok()?;
27813 days = days.checked_add(n32.checked_mul(7)?)?;
27814 }
27815 "month" => {
27816 let n32 = i32::try_from(n).ok()?;
27817 months = months.checked_add(n32)?;
27818 }
27819 "year" => {
27820 let n32 = i32::try_from(n).ok()?;
27821 months = months.checked_add(n32.checked_mul(12)?)?;
27822 }
27823 // v7.39 (read01 timestamp.c) — the larger calendar units.
27824 "decade" => {
27825 let n32 = i32::try_from(n).ok()?;
27826 months = months.checked_add(n32.checked_mul(120)?)?;
27827 }
27828 "century" => {
27829 let n32 = i32::try_from(n).ok()?;
27830 months = months.checked_add(n32.checked_mul(1200)?)?;
27831 }
27832 "millennium" => {
27833 let n32 = i32::try_from(n).ok()?;
27834 months = months.checked_add(n32.checked_mul(12000)?)?;
27835 }
27836 _ => return None,
27837 }
27838 } else if let Ok(f) = parts[i].parse::<f64>() {
27839 // Fractional units cascade down to the next-finer field the way
27840 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27841 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27842 // no_std: f64 has no trunc/fract/round methods, so do them with
27843 // casts (toward-zero) + explicit round-half-away-from-zero.
27844 #[allow(clippy::cast_possible_truncation)]
27845 fn round_i64(x: f64) -> i64 {
27846 if x >= 0.0 {
27847 (x + 0.5) as i64
27848 } else {
27849 (x - 0.5) as i64
27850 }
27851 }
27852 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27853 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27854 const DAY_US: f64 = 86_400_000_000.0;
27855 let whole = d as i64; // truncates toward zero
27856 let frac = d - whole as f64;
27857 *days = days.checked_add(i32::try_from(whole).ok()?)?;
27858 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27859 Some(())
27860 }
27861 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27862 match unit_stripped {
27863 "microsecond" => micros = micros.checked_add(round_i64(f))?,
27864 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27865 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27866 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27867 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27868 "day" => add_days_frac(&mut days, &mut micros, f)?,
27869 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27870 "month" => {
27871 let whole = f as i64;
27872 months = months.checked_add(i32::try_from(whole).ok()?)?;
27873 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27874 }
27875 "year" => {
27876 let m = f * 12.0;
27877 let whole = m as i64;
27878 months = months.checked_add(i32::try_from(whole).ok()?)?;
27879 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27880 }
27881 _ => return None,
27882 }
27883 } else {
27884 return None;
27885 }
27886 i += 2;
27887 }
27888 finish(Some((months, days, micros)))
27889}
27890
27891/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27892/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27893/// `interval` is intentionally absent (handled by its own parser arm).
27894/// Returns `None` for names that aren't sensible as a bare typed literal, so
27895/// the caller falls back to treating the ident as a column reference.
27896fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27897 Some(match ident {
27898 "date" => CastTarget::Date,
27899 "timestamp" | "datetime" => CastTarget::Timestamp,
27900 "timestamptz" => CastTarget::Timestamptz,
27901 "bool" | "boolean" => CastTarget::Bool,
27902 "int" | "integer" | "int4" => CastTarget::Int,
27903 "bigint" | "int8" => CastTarget::BigInt,
27904 "float8" | "double precision" => CastTarget::Float,
27905 "uuid" => CastTarget::Uuid,
27906 "bytea" => CastTarget::Bytea,
27907 "json" => CastTarget::Json,
27908 "jsonb" => CastTarget::Jsonb,
27909 // Types without a dedicated CastTarget variant flow through the
27910 // generic Named path (engine resolves via column_type_to_data_type).
27911 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27912 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27913 | "money" | "bit" | "varbit"
27914 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27915 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27916 // Range / multirange types likewise.
27917 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27918 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27919 | "datemultirange" | "tsmultirange" | "tstzmultirange"
27920 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27921 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27922 CastTarget::Named(alloc::string::String::from(ident))
27923 }
27924 _ => return None,
27925 })
27926}
27927
27928/// v7.12.4 — map a bare type-name identifier (the form that
27929/// appears in a function arg list or RETURNS clause) to a
27930/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27931/// types so the caller can preserve them as
27932/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27933///
27934/// Subset of the full column-type grammar — we deliberately
27935/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27936/// here because function-arg types in v7.12.4 are mostly the
27937/// bare form (`text`, `int`, `bytea`, …).
27938/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27939/// than being `name TYPE`?
27940///
27941/// The multi-word spellings SQL allows for a bare argument type, each
27942/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27943///
27944/// NOTE this list also exists in `spg-storage`, which computes the
27945/// signature key from the rendered argument text and has to reach the
27946/// same verdict. The two crates are siblings — neither depends on the
27947/// other — and each already carries its own table of type spellings
27948/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27949/// there), so this follows the structure rather than inventing new
27950/// duplication. Recorded as V49.
27951pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27952 let t = phrase.trim().to_ascii_lowercase();
27953 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27954 matches!(
27955 base,
27956 "double precision"
27957 | "character varying"
27958 | "bit varying"
27959 | "timestamp with time zone"
27960 | "timestamp without time zone"
27961 | "time with time zone"
27962 | "time without time zone"
27963 | "national character"
27964 | "national character varying"
27965 )
27966}
27967
27968fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27969 Some(match ident.to_ascii_lowercase().as_str() {
27970 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27971 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27972 "bigint" => ColumnTypeName::BigInt,
27973 "float" | "double" => ColumnTypeName::Float,
27974 // v7.39 (round 269) — real is 32-bit.
27975 "real" | "float4" => ColumnTypeName::Real,
27976 "text" => ColumnTypeName::Text,
27977 "bool" | "boolean" => ColumnTypeName::Bool,
27978 "date" => ColumnTypeName::Date,
27979 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27980 "timestamptz" => ColumnTypeName::Timestamptz,
27981 "json" => ColumnTypeName::Json,
27982 "jsonb" => ColumnTypeName::Jsonb,
27983 "bytea" | "bytes" => ColumnTypeName::Bytes,
27984 "tsvector" => ColumnTypeName::TsVector,
27985 "tsquery" => ColumnTypeName::TsQuery,
27986 "uuid" => ColumnTypeName::Uuid,
27987 "interval" => ColumnTypeName::Interval,
27988 "time" => ColumnTypeName::Time,
27989 "year" => ColumnTypeName::Year,
27990 "timetz" => ColumnTypeName::TimeTz,
27991 "money" => ColumnTypeName::Money,
27992 _ => return None,
27993 })
27994}
27995
27996/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27997/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27998///
27999/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28000/// / embedded SQL land in v7.12.5+):
28001///
28002/// ```text
28003/// body := [ws] block [ws]
28004/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28005/// stmt := assign | return
28006/// assign := assign_target := expr
28007/// assign_target := ( NEW | OLD ) . ident | ident
28008/// return := RETURN ( NEW | OLD | NULL | expr )
28009/// ```
28010///
28011/// `expr` is parsed by recursing into the regular `Parser` — so a
28012/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28013/// NEW.subject || ' ' || NEW.sender)` body shape works without
28014/// the body parser knowing what `to_tsvector` is.
28015///
28016/// Errors here cause the caller to fall back to
28017/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28018/// successful, but the executor will refuse to invoke the
28019/// function with an "unparseable body" error.
28020/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28021/// from the crate root as `spg_sql::parse_function_body`.
28022pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28023 parse_plpgsql_body(body)
28024}
28025
28026fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28027 // Use the regular lexer on the body text. The trailing
28028 // `END;` may or may not have a semicolon; the lexer treats
28029 // both forms identically.
28030 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28031 message: alloc::format!("plpgsql body lex error: {e}"),
28032 token_pos: 0,
28033 })?;
28034 let mut parser = Parser::new(tokens);
28035 parser.parse_plpgsql_block()
28036}
28037
28038/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28039fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28040 match v {
28041 crate::ast::SetValue::String(s)
28042 | crate::ast::SetValue::Ident(s)
28043 | crate::ast::SetValue::Number(s) => s.clone(),
28044 crate::ast::SetValue::Default => "DEFAULT".into(),
28045 }
28046}
28047
28048/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28049/// contains an aggregate call at ITS OWN query level (recursion stops at
28050/// sublink boundaries — a sublink's aggregates belong to the sublink).
28051/// Backs the "aggregate functions are not allowed in a recursive query's
28052/// recursive term" well-formedness check.
28053/// v7.40.0 — is this the name of an aggregate? The same list
28054/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28055/// rewrite can leave an aggregate's ARGUMENT alone.
28056pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28057 AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28058}
28059
28060const AGG_NAMES: &[&str] = &[
28061 "count",
28062 "sum",
28063 "min",
28064 "max",
28065 "avg",
28066 "string_agg",
28067 "array_agg",
28068 "bool_and",
28069 "bool_or",
28070 "every",
28071 "any_value",
28072 "json_agg",
28073 "jsonb_agg",
28074 "json_object_agg",
28075 "jsonb_object_agg",
28076 "bit_and",
28077 "bit_or",
28078 "bit_xor",
28079 "var_pop",
28080 "var_samp",
28081 "variance",
28082 "std",
28083 "stddev",
28084 "stddev_pop",
28085 "stddev_samp",
28086 "range_agg",
28087 "range_intersect_agg",
28088 "percentile_cont",
28089 "percentile_disc",
28090 "mode",
28091 "corr",
28092 "covar_pop",
28093 "covar_samp",
28094];
28095
28096fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28097 match e {
28098 Expr::AggregateOrdered { .. } => true,
28099 Expr::FunctionCall { name, args } => {
28100 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28101 || args.iter().any(expr_has_toplevel_aggregate)
28102 }
28103 Expr::NamedArg { expr, .. }
28104 | Expr::Variadic(expr)
28105 | Expr::Unary { expr, .. }
28106 | Expr::Cast { expr, .. }
28107 | Expr::IsNull { expr, .. }
28108 | Expr::FieldAccess { base: expr, .. }
28109 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28110 Expr::Binary { lhs, rhs, .. } => {
28111 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28112 }
28113 Expr::Like { expr, pattern, .. } => {
28114 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28115 }
28116 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28117 Expr::InList { expr, list, .. } => {
28118 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28119 }
28120 Expr::ArraySubscript { target, index } => {
28121 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28122 }
28123 Expr::ArraySlice { target, lo, hi } => {
28124 expr_has_toplevel_aggregate(target)
28125 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28126 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28127 }
28128 Expr::AnyAll { expr, array, .. } => {
28129 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28130 }
28131 Expr::Case {
28132 operand,
28133 branches,
28134 else_branch,
28135 } => {
28136 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28137 || branches
28138 .iter()
28139 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28140 || else_branch
28141 .as_deref()
28142 .is_some_and(expr_has_toplevel_aggregate)
28143 }
28144 // The outer-level operands of a sublink can aggregate; the sublink's
28145 // own body cannot leak its aggregates up here.
28146 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28147 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28148 row.iter().any(expr_has_toplevel_aggregate)
28149 }
28150 _ => false,
28151 }
28152}
28153
28154/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28155/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28156/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28157/// sublink and is legal in a recursive term, so it is not walked here.
28158fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28159 let mut exprs: Vec<&Expr> = Vec::new();
28160 for it in &s.items {
28161 if let crate::ast::SelectItem::Expr { expr, .. } = it {
28162 exprs.push(expr);
28163 }
28164 }
28165 if let Some(w) = &s.where_ {
28166 exprs.push(w);
28167 }
28168 if let Some(h) = &s.having {
28169 exprs.push(h);
28170 }
28171 if let Some(g) = &s.group_by {
28172 exprs.extend(g.iter());
28173 }
28174 if let Some(from) = &s.from {
28175 for j in &from.joins {
28176 if let Some(on) = &j.on {
28177 exprs.push(on);
28178 }
28179 }
28180 }
28181 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28182}
28183
28184/// Does this expression contain a sublink whose subquery mentions `name`?
28185fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28186 match e {
28187 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28188 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28189 Expr::InSubquery { expr, subquery, .. } => {
28190 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28191 }
28192 Expr::RowInSubquery { row, subquery, .. } => {
28193 row.iter().any(|x| expr_sublink_mentions(x, name))
28194 || select_mentions_table(subquery, name)
28195 }
28196 Expr::RowCmpSubquery { row, subquery, .. } => {
28197 row.iter().any(|x| expr_sublink_mentions(x, name))
28198 || select_mentions_table(subquery, name)
28199 }
28200 Expr::NamedArg { expr, .. }
28201 | Expr::Variadic(expr)
28202 | Expr::Unary { expr, .. }
28203 | Expr::Cast { expr, .. }
28204 | Expr::IsNull { expr, .. }
28205 | Expr::FieldAccess { base: expr, .. }
28206 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28207 Expr::Binary { lhs, rhs, .. } => {
28208 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28209 }
28210 Expr::Like { expr, pattern, .. } => {
28211 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28212 }
28213 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28214 args.iter().any(|x| expr_sublink_mentions(x, name))
28215 }
28216 Expr::InList { expr, list, .. } => {
28217 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28218 }
28219 Expr::ArraySubscript { target, index } => {
28220 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28221 }
28222 Expr::ArraySlice { target, lo, hi } => {
28223 expr_sublink_mentions(target, name)
28224 || lo
28225 .as_deref()
28226 .is_some_and(|x| expr_sublink_mentions(x, name))
28227 || hi
28228 .as_deref()
28229 .is_some_and(|x| expr_sublink_mentions(x, name))
28230 }
28231 Expr::AnyAll { expr, array, .. } => {
28232 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28233 }
28234 Expr::Case {
28235 operand,
28236 branches,
28237 else_branch,
28238 } => {
28239 operand
28240 .as_deref()
28241 .is_some_and(|x| expr_sublink_mentions(x, name))
28242 || branches
28243 .iter()
28244 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28245 || else_branch
28246 .as_deref()
28247 .is_some_and(|x| expr_sublink_mentions(x, name))
28248 }
28249 _ => false,
28250 }
28251}
28252
28253/// Does this SELECT (in full — FROM tables, derived tables, its own
28254/// sublinks, and union arms) mention the named table?
28255fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28256 if let Some(from) = &s.from {
28257 if from.primary.name.eq_ignore_ascii_case(name) {
28258 return true;
28259 }
28260 if let Some(sub) = &from.primary.lateral_subquery
28261 && select_mentions_table(sub, name)
28262 {
28263 return true;
28264 }
28265 for j in &from.joins {
28266 if j.table.name.eq_ignore_ascii_case(name) {
28267 return true;
28268 }
28269 if let Some(sub) = &j.table.lateral_subquery
28270 && select_mentions_table(sub, name)
28271 {
28272 return true;
28273 }
28274 }
28275 }
28276 if select_has_self_ref_in_sublink(s, name) {
28277 return true;
28278 }
28279 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28280}
28281
28282/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28283/// row count, the way PG evaluates one before applying it.
28284///
28285/// `None` = not a constant (a column, a subquery, a function call).
28286/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28287/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28288/// All wordings were read off live PG 18.4.
28289fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28290 use crate::ast::{BinOp, Expr, Literal, UnOp};
28291 match e {
28292 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28293 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28294 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28295 }
28296 // PG coerces a string by its CONTENT, and fails on the value.
28297 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28298 |_| {
28299 Err(alloc::format!(
28300 "invalid input syntax for type bigint: \"{t}\""
28301 ))
28302 },
28303 |n| Ok(i128::from(n)),
28304 )),
28305 Expr::Literal(Literal::Bool(_)) => Some(Err(
28306 "argument of {L} must be type bigint, not type boolean".into(),
28307 )),
28308 Expr::Unary {
28309 op: UnOp::Neg,
28310 expr,
28311 } => match fold_limit_constant(expr)? {
28312 Ok(v) => Some(Ok(-v)),
28313 e @ Err(_) => Some(e),
28314 },
28315 Expr::Binary { lhs, op, rhs } => {
28316 let a = match fold_limit_constant(lhs)? {
28317 Ok(v) => v,
28318 e @ Err(_) => return Some(e),
28319 };
28320 let b = match fold_limit_constant(rhs)? {
28321 Ok(v) => v,
28322 e @ Err(_) => return Some(e),
28323 };
28324 let out = match op {
28325 BinOp::Add => a.checked_add(b),
28326 BinOp::Sub => a.checked_sub(b),
28327 BinOp::Mul => a.checked_mul(b),
28328 BinOp::Div if b != 0 => a.checked_div(b),
28329 BinOp::Div => return Some(Err("division by zero".into())),
28330 BinOp::Mod if b != 0 => a.checked_rem(b),
28331 BinOp::Mod => return Some(Err("division by zero".into())),
28332 _ => return None,
28333 };
28334 // PG evaluates the arithmetic in the operand's own type, so an
28335 // int-by-int product that leaves int range fails there — before
28336 // the row count is ever looked at.
28337 match out {
28338 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28339 Some(Err("integer out of range".into()))
28340 }
28341 Some(v) => Some(Ok(v)),
28342 None => Some(Err("integer out of range".into())),
28343 }
28344 }
28345 _ => None,
28346 }
28347}
28348
28349/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28350/// cast, which is what makes `LIMIT 2.5` keep three rows.
28351fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28352 if scale == 0 {
28353 return unscaled;
28354 }
28355 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28356 return 0;
28357 };
28358 let neg = unscaled < 0;
28359 let mag = unscaled.unsigned_abs() as i128;
28360 let rounded = (mag + div / 2) / div;
28361 if neg { -rounded } else { rounded }
28362}
28363
28364#[cfg(test)]
28365mod tests {
28366 use super::*;
28367 use alloc::string::ToString;
28368
28369 fn parse(s: &str) -> Statement {
28370 parse_statement(s).expect("parse ok")
28371 }
28372
28373 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28374 // `tables`, `partition`, etc. are unreserved keywords per PG's
28375 // `pg_get_keywords()` and MUST be usable as column / table /
28376 // alias names. Pre-T4 every drop-in user whose schema had one
28377 // of these as a column name (sentori events.release, mailrs
28378 // messages.index in some forks) blew the parser up at CREATE
28379 // TABLE time with "expected identifier, got Release". The
28380 // generalisation lives in `unreserved_keyword_text` + the
28381 // `expect_ident_like` and `parse_atom` arms that consult it.
28382 #[test]
28383 fn release_usable_as_column_name_in_create_table() {
28384 let stmt =
28385 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28386 if let Statement::CreateTable(t) = stmt {
28387 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28388 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28389 } else {
28390 panic!("expected CreateTable");
28391 }
28392 }
28393
28394 #[test]
28395 fn release_usable_as_column_ref_in_select_projection() {
28396 // The sentori `0003_partition_events.sql` INSERT-SELECT
28397 // walk references `release` in both column lists; the
28398 // projection-side use exercises `parse_atom`'s relaxed
28399 // identifier set.
28400 parse("SELECT id, release, payload FROM events WHERE id = 1");
28401 }
28402
28403 #[test]
28404 fn release_usable_as_column_ref_in_insert_column_list() {
28405 // INSERT INTO t (id, release, payload) VALUES (…)
28406 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28407 }
28408
28409 #[test]
28410 fn alter_column_drop_not_null_uses_keyword_drop_token() {
28411 // Sentori `0013_audit_tombstone.sql` issues
28412 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28413 // emits Token::Drop (not Ident("drop")); the parser must
28414 // accept both in the ALTER COLUMN sub-dispatch.
28415 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28416 }
28417
28418 #[test]
28419 fn create_index_accepts_parenthesised_expression_key() {
28420 // sentori `0040_events_bundle_idx.sql` shape — JSONB
28421 // expression index. Pre-T4 the parser bailed at the
28422 // inner `(` with "expected column ident or expression,
28423 // got LParen". The Token::LParen arm in CREATE INDEX
28424 // routes through the expression parser instead.
28425 parse(
28426 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28427 ON events ((payload->'bundle'->>'id'))",
28428 );
28429 }
28430
28431 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28432 // surface as parse errors, never stack overflows (embed hosts
28433 // abort on overflow).
28434 /// The nesting budget is a COUNT; what it has to fit inside is a
28435 /// number of BYTES, and only one of those two is stable across
28436 /// compiler versions. Round 847 measured 30,336 bytes per level
28437 /// after a toolchain move, which puts 64 levels at 1.94 MB and
28438 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28439 /// aborted instead of erroring, which is precisely the outcome it
28440 /// exists to rule out.
28441 ///
28442 /// So the budget is metered rather than assumed. The ceiling leaves
28443 /// the depth SPG advertises fitting in a default 2 MiB thread with
28444 /// room to spare, in the debug build, where frames are widest.
28445 #[test]
28446 fn nesting_frame_cost_stays_under_ceiling() {
28447 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28448 // thread keeps a margin for whatever called the parser.
28449 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28450
28451 frame_meter::reset();
28452 let depth = frame_meter::SAMPLE_HI + 8;
28453 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28454 parse(&sql);
28455
28456 let per_level = frame_meter::bytes_per_level();
28457 {
28458 extern crate std;
28459 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28460 }
28461 assert!(
28462 per_level <= CEILING,
28463 "{per_level} bytes per nesting level exceeds {CEILING}; \
28464 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28465 in parse_expr_inner / parse_unary rather than lowering the \
28466 depth or widening the stack.",
28467 per_level * MAX_NEST_DEPTH
28468 );
28469 }
28470
28471 #[test]
28472 fn nesting_budget_errors_cleanly() {
28473 let depth = MAX_NEST_DEPTH + 50;
28474 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28475 let err = parse_statement(&sql).expect_err("must reject");
28476 assert!(err.message.contains("nests deeper"), "{err:?}");
28477 // Within budget still parses.
28478 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28479 parse(&sql);
28480 }
28481
28482 #[test]
28483 fn binary_chain_budget_errors_cleanly() {
28484 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28485 let err = parse_statement(&sql).expect_err("must reject");
28486 assert!(err.message.contains("chained binary"), "{err:?}");
28487 // Within budget still parses (chain depth ≤ budget is safe
28488 // for recursive eval/drop on 2 MiB stacks).
28489 let sql = format!("SELECT 1{}", " + 1".repeat(200));
28490 parse(&sql);
28491 }
28492
28493 #[test]
28494 fn in_list_unaffected_by_chain_budget() {
28495 // Flat InList: 20k elements parse fine and stay flat.
28496 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28497 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28498 let Statement::Select(s) = parse(&sql) else {
28499 panic!("expected select")
28500 };
28501 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28502 panic!("expected flat InList, got {:?}", s.where_)
28503 };
28504 assert_eq!(list.len(), 20_000);
28505 assert!(!negated);
28506 }
28507
28508 fn lit_int(n: i64) -> Expr {
28509 Expr::Literal(Literal::Integer(n))
28510 }
28511
28512 fn col(name: &str) -> Expr {
28513 Expr::Column(ColumnName {
28514 qualifier: None,
28515 name: name.into(),
28516 })
28517 }
28518
28519 #[test]
28520 fn select_single_integer() {
28521 let s = parse("SELECT 1");
28522 let Statement::Select(s) = s else {
28523 panic!("expected SELECT")
28524 };
28525 assert_eq!(s.items.len(), 1);
28526 assert!(s.from.is_none());
28527 assert!(s.where_.is_none());
28528 }
28529
28530 #[test]
28531 fn select_multiple_literal_kinds() {
28532 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28533 let Statement::Select(s) = s else {
28534 panic!("expected SELECT")
28535 };
28536 assert_eq!(s.items.len(), 5);
28537 }
28538
28539 #[test]
28540 fn select_wildcard_from_table() {
28541 let s = parse("SELECT * FROM users");
28542 let Statement::Select(s) = s else {
28543 panic!("expected SELECT")
28544 };
28545 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28546 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28547 }
28548
28549 #[test]
28550 fn select_with_table_alias() {
28551 let s = parse("SELECT * FROM users AS u");
28552 let Statement::Select(s) = s else {
28553 panic!("expected SELECT")
28554 };
28555 let t = &s.from.as_ref().unwrap().primary;
28556 assert_eq!(t.name, "users");
28557 assert_eq!(t.alias.as_deref(), Some("u"));
28558 }
28559
28560 #[test]
28561 fn select_with_where_eq() {
28562 let s = parse("SELECT a FROM t WHERE a = 1");
28563 let Statement::Select(s) = s else {
28564 panic!("expected SELECT")
28565 };
28566 let w = s.where_.unwrap();
28567 assert_eq!(
28568 w,
28569 Expr::Binary {
28570 lhs: Box::new(col("a")),
28571 op: BinOp::Eq,
28572 rhs: Box::new(lit_int(1)),
28573 }
28574 );
28575 }
28576
28577 #[test]
28578 fn arithmetic_precedence() {
28579 let s = parse("SELECT 1 + 2 * 3");
28580 let Statement::Select(s) = s else {
28581 panic!("expected SELECT")
28582 };
28583 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28584 panic!("wildcard?")
28585 };
28586 assert_eq!(
28587 expr,
28588 &Expr::Binary {
28589 lhs: Box::new(lit_int(1)),
28590 op: BinOp::Add,
28591 rhs: Box::new(Expr::Binary {
28592 lhs: Box::new(lit_int(2)),
28593 op: BinOp::Mul,
28594 rhs: Box::new(lit_int(3)),
28595 }),
28596 }
28597 );
28598 }
28599
28600 #[test]
28601 fn parentheses_override_precedence() {
28602 let s = parse("SELECT (1 + 2) * 3");
28603 let Statement::Select(s) = s else {
28604 panic!("expected SELECT")
28605 };
28606 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28607 panic!()
28608 };
28609 assert_eq!(
28610 expr,
28611 &Expr::Binary {
28612 lhs: Box::new(Expr::Binary {
28613 lhs: Box::new(lit_int(1)),
28614 op: BinOp::Add,
28615 rhs: Box::new(lit_int(2)),
28616 }),
28617 op: BinOp::Mul,
28618 rhs: Box::new(lit_int(3)),
28619 }
28620 );
28621 }
28622
28623 #[test]
28624 fn not_binds_below_comparison() {
28625 // `NOT a = 1` should parse as `NOT (a = 1)`.
28626 let s = parse("SELECT NOT a = 1 FROM t");
28627 let Statement::Select(s) = s else {
28628 panic!("expected SELECT")
28629 };
28630 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28631 panic!()
28632 };
28633 assert_eq!(
28634 expr,
28635 &Expr::Unary {
28636 op: UnOp::Not,
28637 expr: Box::new(Expr::Binary {
28638 lhs: Box::new(col("a")),
28639 op: BinOp::Eq,
28640 rhs: Box::new(lit_int(1)),
28641 }),
28642 }
28643 );
28644 }
28645
28646 #[test]
28647 fn unary_minus_binds_above_multiplication() {
28648 // `-a * 2` should be `(-a) * 2`.
28649 let s = parse("SELECT -a * 2 FROM t");
28650 let Statement::Select(s) = s else {
28651 panic!("expected SELECT")
28652 };
28653 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28654 panic!()
28655 };
28656 assert_eq!(
28657 expr,
28658 &Expr::Binary {
28659 lhs: Box::new(Expr::Unary {
28660 op: UnOp::Neg,
28661 expr: Box::new(col("a")),
28662 }),
28663 op: BinOp::Mul,
28664 rhs: Box::new(lit_int(2)),
28665 }
28666 );
28667 }
28668
28669 #[test]
28670 fn qualified_column() {
28671 let s = parse("SELECT t.col FROM t");
28672 let Statement::Select(s) = s else {
28673 panic!("expected SELECT")
28674 };
28675 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28676 panic!()
28677 };
28678 assert_eq!(
28679 expr,
28680 &Expr::Column(ColumnName {
28681 qualifier: Some("t".into()),
28682 name: "col".into()
28683 })
28684 );
28685 }
28686
28687 #[test]
28688 fn select_item_alias_with_as() {
28689 let s = parse("SELECT a AS y FROM t");
28690 let Statement::Select(s) = s else {
28691 panic!("expected SELECT")
28692 };
28693 let SelectItem::Expr { alias, .. } = &s.items[0] else {
28694 panic!()
28695 };
28696 assert_eq!(alias.as_deref(), Some("y"));
28697 }
28698
28699 #[test]
28700 fn trailing_semicolon_accepted() {
28701 let s = parse("SELECT 1;");
28702 let Statement::Select(s) = s else {
28703 panic!("expected SELECT")
28704 };
28705 assert_eq!(s.items.len(), 1);
28706 }
28707
28708 #[test]
28709 fn boolean_chain_with_and_or_not() {
28710 // (NOT a) OR (b AND (NOT c))
28711 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28712 let Statement::Select(s) = s else {
28713 panic!("expected SELECT")
28714 };
28715 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28716 panic!()
28717 };
28718 let expected = Expr::Binary {
28719 lhs: Box::new(Expr::Unary {
28720 op: UnOp::Not,
28721 expr: Box::new(col("a")),
28722 }),
28723 op: BinOp::Or,
28724 rhs: Box::new(Expr::Binary {
28725 lhs: Box::new(col("b")),
28726 op: BinOp::And,
28727 rhs: Box::new(Expr::Unary {
28728 op: UnOp::Not,
28729 expr: Box::new(col("c")),
28730 }),
28731 }),
28732 };
28733 assert_eq!(expr, &expected);
28734 }
28735
28736 #[test]
28737 fn empty_input_errors() {
28738 // v7.14.0 — pg_dump preambles emit several comment-only
28739 // / blank-line statements that collapse to Statement::
28740 // Empty rather than a parse error. The old "SELECT in
28741 // message" assertion is stale; verify the new contract:
28742 // empty / whitespace / comment-only input parses to
28743 // Statement::Empty.
28744 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28745 assert!(matches!(
28746 parse_statement(" \n\t ").unwrap(),
28747 Statement::Empty
28748 ));
28749 // Sanity: malformed-but-non-empty still errors.
28750 assert!(parse_statement("SELECT FROM WHERE").is_err());
28751 }
28752
28753 #[test]
28754 fn unmatched_paren_errors() {
28755 assert!(parse_statement("SELECT (1 + 2").is_err());
28756 }
28757
28758 #[test]
28759 fn display_round_trip_simple_select() {
28760 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28761 let text = original.to_string();
28762 let again = parse_statement(&text).expect("re-parse");
28763 assert_eq!(original, again);
28764 }
28765
28766 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28767
28768 #[test]
28769 fn create_table_single_column() {
28770 let s = parse("CREATE TABLE foo (a INT)");
28771 let Statement::CreateTable(c) = s else {
28772 panic!("expected CreateTable")
28773 };
28774 assert_eq!(c.name, "foo");
28775 assert_eq!(c.columns.len(), 1);
28776 assert_eq!(c.columns[0].name, "a");
28777 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28778 assert!(c.columns[0].nullable);
28779 }
28780
28781 #[test]
28782 fn create_table_multi_column_with_not_null_mix() {
28783 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28784 let Statement::CreateTable(c) = s else {
28785 panic!()
28786 };
28787 assert_eq!(c.columns.len(), 4);
28788 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28789 assert!(!c.columns[0].nullable);
28790 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28791 assert!(c.columns[1].nullable);
28792 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28793 assert!(!c.columns[2].nullable);
28794 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28795 }
28796
28797 #[test]
28798 fn create_table_bigint_supported() {
28799 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28800 let Statement::CreateTable(c) = s else {
28801 panic!()
28802 };
28803 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28804 }
28805
28806 #[test]
28807 fn create_table_vector_default_is_f32() {
28808 let s = parse("CREATE TABLE t (v VECTOR(128))");
28809 let Statement::CreateTable(c) = s else {
28810 panic!()
28811 };
28812 assert_eq!(
28813 c.columns[0].ty,
28814 ColumnTypeName::Vector {
28815 dim: 128,
28816 encoding: VecEncoding::F32,
28817 },
28818 );
28819 }
28820
28821 #[test]
28822 fn create_table_vector_using_sq8() {
28823 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28824 // Case-insensitive on both `USING` and the encoding name.
28825 for sql in [
28826 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28827 "CREATE TABLE t (v VECTOR(128) using sq8)",
28828 ] {
28829 let s = parse(sql);
28830 let Statement::CreateTable(c) = s else {
28831 panic!()
28832 };
28833 assert_eq!(
28834 c.columns[0].ty,
28835 ColumnTypeName::Vector {
28836 dim: 128,
28837 encoding: VecEncoding::Sq8,
28838 },
28839 "{sql}",
28840 );
28841 }
28842 }
28843
28844 #[test]
28845 fn create_table_vector_using_unknown_errors() {
28846 // v7.16.1 — the inline `USING <encoding>` shape on
28847 // CREATE TABLE column defs was withdrawn before
28848 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28849 // (col vector_<metric>_ops)`; the parser now rejects
28850 // USING at column-list position with a clearer
28851 // "expected ',' or ')'" message. Test asserts the
28852 // current rejection, not the old "unknown vector
28853 // encoding" string.
28854 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28855 assert!(
28856 err.message.contains("USING")
28857 || err.message.contains("using")
28858 || err.message.contains("')'")
28859 || err.message.contains("','"),
28860 "expected USING/column-list rejection, got: {}",
28861 err.message
28862 );
28863 }
28864
28865 #[test]
28866 fn vector_using_sq8_display_roundtrips() {
28867 // The Display impl must produce text that re-parses to the
28868 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28869 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28870 let Statement::CreateTable(c) = s else {
28871 panic!()
28872 };
28873 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28874 }
28875
28876 #[test]
28877 fn parser_recognises_placeholders() {
28878 use crate::ast::{Expr, SelectItem, Statement};
28879 // $N in expression position parses as Expr::Placeholder(N).
28880 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28881 let Statement::Select(sel) = s else { panic!() };
28882 assert!(matches!(
28883 sel.items[0],
28884 SelectItem::Expr {
28885 expr: Expr::Placeholder(1),
28886 alias: None
28887 }
28888 ));
28889 // $2 + 1
28890 let SelectItem::Expr {
28891 expr: Expr::Binary { lhs, rhs, .. },
28892 ..
28893 } = &sel.items[1]
28894 else {
28895 panic!()
28896 };
28897 assert!(matches!(**lhs, Expr::Placeholder(2)));
28898 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28899 // WHERE x = $3
28900 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28901 panic!()
28902 };
28903 assert!(matches!(**rhs, Expr::Placeholder(3)));
28904 }
28905
28906 #[test]
28907 fn parser_rejects_dollar_zero() {
28908 // $0 is not valid in PG; the lexer rejects it.
28909 assert!(parse_statement("SELECT $0").is_err());
28910 }
28911
28912 #[test]
28913 fn placeholder_display_roundtrips() {
28914 // The Display impl must produce text that re-lexes to the
28915 // same Placeholder token.
28916 let s = parse("SELECT $42 FROM t");
28917 let printed = s.to_string();
28918 assert!(printed.contains("$42"));
28919 let again = parse(&printed);
28920 assert_eq!(s, again);
28921 }
28922
28923 #[test]
28924 fn alter_index_rebuild_bare() {
28925 use crate::ast::{AlterIndexTarget, Statement};
28926 let s = parse("ALTER INDEX my_idx REBUILD");
28927 let Statement::AlterIndex(a) = s else {
28928 panic!("expected AlterIndex, got {s:?}")
28929 };
28930 assert_eq!(a.name, "my_idx");
28931 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28932 }
28933
28934 #[test]
28935 fn alter_index_rebuild_with_encoding() {
28936 use crate::ast::{AlterIndexTarget, Statement};
28937 for (sql, want) in [
28938 (
28939 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28940 VecEncoding::F32,
28941 ),
28942 (
28943 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28944 VecEncoding::Sq8,
28945 ),
28946 (
28947 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28948 VecEncoding::F16,
28949 ),
28950 ] {
28951 let s = parse(sql);
28952 let Statement::AlterIndex(a) = s else {
28953 panic!("{sql}: expected AlterIndex")
28954 };
28955 assert_eq!(a.name, "my_idx");
28956 assert_eq!(
28957 a.target,
28958 AlterIndexTarget::Rebuild {
28959 encoding: Some(want)
28960 },
28961 "{sql}"
28962 );
28963 }
28964 }
28965
28966 #[test]
28967 fn alter_index_rebuild_unknown_encoding_errors() {
28968 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28969 assert!(
28970 err.message.contains("unknown vector encoding"),
28971 "got: {}",
28972 err.message
28973 );
28974 }
28975
28976 #[test]
28977 fn alter_index_rebuild_display_roundtrips() {
28978 for (input, want) in [
28979 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28980 (
28981 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28982 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28983 ),
28984 (
28985 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28986 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28987 ),
28988 ] {
28989 let s = parse(input);
28990 assert_eq!(s.to_string(), want);
28991 }
28992 }
28993
28994 #[test]
28995 fn create_table_unknown_type_defers_to_engine() {
28996 // v4.9 picked XML as a parse-time "unsupported column
28997 // type" probe. v7.17.0 Phase 1.4 changed the contract:
28998 // an unknown type ident parses as Text + `user_type_ref`
28999 // so CREATE TABLE can resolve user-defined enum / domain
29000 // types — rejection of truly-unknown types moved to the
29001 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29002 // to a first-class built-in, so this probe switched to a
29003 // synthetic name nothing in the lexer will ever recognise.
29004 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29005 let Statement::CreateTable(t) = stmt else {
29006 panic!("expected CreateTable");
29007 };
29008 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29009 }
29010
29011 #[test]
29012 fn create_table_missing_table_keyword_errors() {
29013 assert!(parse_statement("CREATE x (a INT)").is_err());
29014 }
29015
29016 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29017 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29018
29019 #[test]
29020 fn parse_create_table_partition_by_range() {
29021 use crate::ast::{PartitionBySpec, PartitionKindAst};
29022 let stmt = parse_statement(
29023 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29024 payload JSONB) PARTITION BY RANGE (ts)",
29025 )
29026 .unwrap();
29027 let Statement::CreateTable(t) = stmt else {
29028 panic!("expected CreateTable");
29029 };
29030 assert!(t.partition_of.is_none(), "parent has no partition_of");
29031 assert_eq!(t.columns.len(), 3);
29032 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29033 assert_eq!(
29034 by,
29035 &PartitionBySpec {
29036 kind: PartitionKindAst::Range,
29037 key_columns: alloc::vec!["ts".to_string()],
29038 }
29039 );
29040 // Display round-trip preserves the suffix. `quote_ident`
29041 // only adds double quotes when the ident needs escaping, so
29042 // a plain `ts` survives bare here.
29043 assert!(
29044 t.to_string().contains("PARTITION BY RANGE (ts)"),
29045 "Display lost PARTITION BY suffix: {t}"
29046 );
29047 }
29048
29049 #[test]
29050 fn parse_create_table_partition_of_range() {
29051 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29052 let stmt = parse_statement(
29053 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29054 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29055 )
29056 .unwrap();
29057 let Statement::CreateTable(t) = stmt else {
29058 panic!("expected CreateTable");
29059 };
29060 assert!(t.columns.is_empty(), "child inherits columns from parent");
29061 assert!(t.partition_by.is_none());
29062 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29063 assert_eq!(of.parent_name, "events_partitioned");
29064 let PartitionOfSpec { bounds, .. } = of.clone();
29065 match bounds {
29066 PartitionOfBoundsAst::Range { lower, upper } => {
29067 assert!(lower.to_string().contains("2026-06-01"));
29068 assert!(upper.to_string().contains("2026-07-01"));
29069 }
29070 other => panic!("expected Range, got {other:?}"),
29071 }
29072 // Display round-trip emits the FOR VALUES tail. `quote_ident`
29073 // skips quotes when not required, so the parent name appears
29074 // bare here.
29075 let s = t.to_string();
29076 assert!(
29077 s.contains("PARTITION OF events_partitioned"),
29078 "Display lost PARTITION OF: {s}"
29079 );
29080 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29081 assert!(s.contains(") TO ("), "Display lost TO: {s}");
29082 }
29083
29084 #[test]
29085 fn parse_create_table_partition_of_default() {
29086 use crate::ast::PartitionOfBoundsAst;
29087 let stmt =
29088 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29089 .unwrap();
29090 let Statement::CreateTable(t) = stmt else {
29091 panic!("expected CreateTable");
29092 };
29093 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29094 assert_eq!(of.parent_name, "events_partitioned");
29095 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29096 assert!(
29097 t.to_string()
29098 .contains("PARTITION OF events_partitioned DEFAULT"),
29099 "Display lost DEFAULT: {t}"
29100 );
29101 }
29102
29103 #[test]
29104 fn parse_create_table_partition_by_list() {
29105 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29106 // child with `FOR VALUES IN (lit, lit, …)`.
29107 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29108 let parent =
29109 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29110 .unwrap();
29111 let Statement::CreateTable(t) = parent else {
29112 panic!("expected CreateTable");
29113 };
29114 let Some(PartitionBySpec {
29115 kind,
29116 ref key_columns,
29117 }) = t.partition_by
29118 else {
29119 panic!("expected PARTITION BY");
29120 };
29121 assert_eq!(kind, PartitionKindAst::List);
29122 assert_eq!(*key_columns, vec!["region".to_string()]);
29123 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29124
29125 let child = parse_statement(
29126 "CREATE TABLE events_apac PARTITION OF events_listed \
29127 FOR VALUES IN ('jp', 'kr', 'tw')",
29128 )
29129 .unwrap();
29130 let Statement::CreateTable(c) = child else {
29131 panic!("expected CreateTable");
29132 };
29133 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29134 let PartitionOfBoundsAst::List { values } = &of.bounds else {
29135 panic!("expected List bounds, got {:?}", of.bounds);
29136 };
29137 assert_eq!(values.len(), 3);
29138 let disp = c.to_string();
29139 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29140 }
29141
29142 #[test]
29143 fn parse_create_table_partition_by_hash() {
29144 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29145 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29146 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29147 let parent =
29148 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29149 let Statement::CreateTable(t) = parent else {
29150 panic!("expected CreateTable");
29151 };
29152 let Some(PartitionBySpec {
29153 kind,
29154 ref key_columns,
29155 }) = t.partition_by
29156 else {
29157 panic!("expected PARTITION BY");
29158 };
29159 assert_eq!(kind, PartitionKindAst::Hash);
29160 assert_eq!(*key_columns, vec!["id".to_string()]);
29161 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29162
29163 let child = parse_statement(
29164 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29165 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29166 )
29167 .unwrap();
29168 let Statement::CreateTable(c) = child else {
29169 panic!("expected CreateTable");
29170 };
29171 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29172 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29173 panic!("expected Hash bounds");
29174 };
29175 assert_eq!(modulus, 4);
29176 assert_eq!(remainder, 0);
29177 let disp = c.to_string();
29178 assert!(
29179 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29180 "Display lost HASH bounds: {disp}"
29181 );
29182
29183 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29184 let bad = parse_statement(
29185 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29186 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29187 );
29188 let msg = format!("{}", bad.unwrap_err());
29189 assert!(
29190 msg.contains("REMAINDER") && msg.contains("MODULUS"),
29191 "expected REMAINDER/MODULUS validation error: {msg}"
29192 );
29193 }
29194
29195 #[test]
29196 fn parse_create_table_partition_of_rejects_columns() {
29197 // v7.37.6-B contract: PARTITION OF children inherit columns
29198 // from the parent; an explicit list MUST surface as a parse
29199 // error rather than getting silently ignored.
29200 let err = parse_statement(
29201 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29202 FOR VALUES FROM ('a') TO ('b')",
29203 );
29204 assert!(err.is_err(), "expected parse error for explicit columns");
29205 let msg = format!("{}", err.unwrap_err());
29206 assert!(
29207 msg.contains("PARTITION OF") && msg.contains("column"),
29208 "error should mention PARTITION OF + columns: {msg}"
29209 );
29210 }
29211
29212 #[test]
29213 fn insert_single_value() {
29214 let s = parse("INSERT INTO foo VALUES (42)");
29215 let Statement::Insert(i) = s else {
29216 panic!("expected Insert")
29217 };
29218 assert_eq!(i.table, "foo");
29219 assert_eq!(i.rows.len(), 1);
29220 assert_eq!(i.rows[0].len(), 1);
29221 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29222 }
29223
29224 #[test]
29225 fn insert_multi_value_with_mixed_literals() {
29226 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29227 let Statement::Insert(i) = s else { panic!() };
29228 assert_eq!(i.rows.len(), 1);
29229 assert_eq!(i.rows[0].len(), 5);
29230 }
29231
29232 #[test]
29233 fn insert_missing_into_errors() {
29234 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29235 }
29236
29237 #[test]
29238 fn create_table_round_trip() {
29239 let original =
29240 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29241 let text = original.to_string();
29242 let again = parse_statement(&text).expect("re-parse");
29243 assert_eq!(original, again);
29244 }
29245
29246 #[test]
29247 fn insert_round_trip_with_negation_and_string() {
29248 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29249 let text = original.to_string();
29250 let again = parse_statement(&text).expect("re-parse");
29251 assert_eq!(original, again);
29252 }
29253
29254 #[test]
29255 fn unknown_keyword_at_statement_start_errors() {
29256 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29257 // the top-level dispatch still has no branch to take.
29258 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29259 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29260 }
29261
29262 // --- v0.8 CREATE INDEX --------------------------------------------------
29263
29264 #[test]
29265 fn create_index_basic() {
29266 let s = parse("CREATE INDEX idx_id ON users (id)");
29267 let Statement::CreateIndex(c) = s else {
29268 panic!("expected CreateIndex")
29269 };
29270 assert_eq!(c.name, "idx_id");
29271 assert_eq!(c.table, "users");
29272 assert_eq!(c.column, "id");
29273 }
29274
29275 #[test]
29276 fn create_index_missing_on_errors() {
29277 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29278 }
29279
29280 #[test]
29281 fn create_index_missing_paren_errors() {
29282 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29283 }
29284
29285 #[test]
29286 fn create_index_round_trip() {
29287 let original = parse("CREATE INDEX by_name ON users (name)");
29288 let again = parse_statement(&original.to_string()).unwrap();
29289 assert_eq!(original, again);
29290 }
29291
29292 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29293
29294 #[test]
29295 fn create_unique_index_basic() {
29296 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29297 let Statement::CreateIndex(c) = s else {
29298 panic!("expected CreateIndex");
29299 };
29300 assert!(c.is_unique);
29301 assert_eq!(c.column, "a");
29302 assert!(c.partial_predicate.is_none());
29303 }
29304
29305 #[test]
29306 fn create_unique_index_partial() {
29307 // mailrs's email_templates "one default per user" shape.
29308 let s = parse(
29309 "CREATE UNIQUE INDEX idx_email_templates_user_default \
29310 ON email_templates (user_address) WHERE is_default = true",
29311 );
29312 let Statement::CreateIndex(c) = s else {
29313 panic!("expected CreateIndex");
29314 };
29315 assert!(c.is_unique);
29316 assert_eq!(c.table, "email_templates");
29317 assert_eq!(c.column, "user_address");
29318 assert!(c.partial_predicate.is_some());
29319 }
29320
29321 #[test]
29322 fn create_unique_index_composite_with_predicate() {
29323 // mailrs's calendar_events instance: composite columns.
29324 let s = parse(
29325 "CREATE UNIQUE INDEX uq_calendar_events_instance \
29326 ON calendar_events (calendar_id, uid, recurrence_id) \
29327 WHERE recurrence_id IS NOT NULL",
29328 );
29329 let Statement::CreateIndex(c) = s else {
29330 panic!("expected CreateIndex");
29331 };
29332 assert!(c.is_unique);
29333 assert_eq!(c.column, "calendar_id");
29334 assert_eq!(
29335 c.extra_columns,
29336 vec!["uid".to_string(), "recurrence_id".to_string()]
29337 );
29338 assert!(c.partial_predicate.is_some());
29339 }
29340
29341 #[test]
29342 fn create_unique_index_using_btree_ok() {
29343 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29344 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29345 }
29346
29347 #[test]
29348 fn create_unique_index_using_hnsw_rejected() {
29349 let err =
29350 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29351 assert!(err.message.contains("UNIQUE"), "{}", err.message);
29352 }
29353
29354 #[test]
29355 fn create_unique_index_round_trip() {
29356 let original = parse(
29357 "CREATE UNIQUE INDEX uq_calendar_events_master \
29358 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29359 );
29360 let again = parse_statement(&original.to_string()).unwrap();
29361 assert_eq!(original, again);
29362 }
29363
29364 #[test]
29365 fn create_unique_without_index_errors() {
29366 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29367 // v7.39 (round 340, V56) — PG 18.4, verbatim.
29368 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29369 }
29370
29371 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29372
29373 #[test]
29374 fn create_table_bytea_column() {
29375 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29376 let Statement::CreateTable(c) = s else {
29377 panic!("expected CreateTable");
29378 };
29379 assert_eq!(c.columns.len(), 2);
29380 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29381 assert!(!c.columns[1].nullable);
29382 }
29383
29384 #[test]
29385 fn create_table_bytes_alias_column() {
29386 let s = parse("CREATE TABLE t (blob BYTES)");
29387 let Statement::CreateTable(c) = s else {
29388 panic!("expected CreateTable");
29389 };
29390 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29391 }
29392
29393 #[test]
29394 fn bytea_round_trip_display() {
29395 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29396 let again = parse_statement(&original.to_string()).unwrap();
29397 assert_eq!(original, again);
29398 }
29399
29400 // --- v0.9 transactions -------------------------------------------------
29401
29402 #[test]
29403 fn begin_commit_rollback_parse_as_unit_variants() {
29404 let plain = crate::ast::TransactionModes::default();
29405 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29406 assert_eq!(parse("COMMIT"), Statement::Commit);
29407 // r1066 — PG synonyms pgbench's tpcb script relies on.
29408 assert_eq!(parse("END"), Statement::Commit);
29409 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29410 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29411 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29412 // Trailing semicolons accepted too.
29413 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29414 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29415 // statement (with or without the WORK/TRANSACTION noise word).
29416 assert_eq!(
29417 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29418 Statement::Begin(crate::ast::TransactionModes {
29419 isolation: Some(IsolationLevel::RepeatableRead),
29420 read_only: None,
29421 })
29422 );
29423 assert_eq!(
29424 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29425 Statement::Begin(crate::ast::TransactionModes {
29426 isolation: Some(IsolationLevel::Serializable),
29427 read_only: None,
29428 })
29429 );
29430 // v7.39 — this line used to read
29431 //
29432 // // A non-isolation mode keeps the session default (None).
29433 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29434 //
29435 // which pinned the defect rather than catching it: the READ ONLY
29436 // was thrown away, so the statement opened an ordinary read-write
29437 // transaction and every write inside it was accepted. The
29438 // isolation level is still absent here, because this statement
29439 // does not name one — that part was right.
29440 assert_eq!(
29441 parse("BEGIN READ ONLY"),
29442 Statement::Begin(crate::ast::TransactionModes {
29443 isolation: None,
29444 read_only: Some(true),
29445 })
29446 );
29447 assert_eq!(
29448 parse("START TRANSACTION READ WRITE"),
29449 Statement::Begin(crate::ast::TransactionModes {
29450 isolation: None,
29451 read_only: Some(false),
29452 })
29453 );
29454 assert_eq!(
29455 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29456 Statement::Begin(crate::ast::TransactionModes {
29457 isolation: Some(IsolationLevel::Serializable),
29458 read_only: Some(true),
29459 })
29460 );
29461 }
29462
29463 // --- v1.2: pgvector distance ops + ::vector cast --------------------
29464
29465 #[test]
29466 fn inner_product_binop_parses() {
29467 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29468 let Statement::Select(s) = s else { panic!() };
29469 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29470 panic!()
29471 };
29472 assert!(matches!(
29473 expr,
29474 Expr::Binary {
29475 op: BinOp::InnerProduct,
29476 ..
29477 }
29478 ));
29479 }
29480
29481 #[test]
29482 fn cosine_distance_binop_parses() {
29483 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29484 let Statement::Select(s) = s else { panic!() };
29485 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29486 panic!()
29487 };
29488 assert!(matches!(
29489 expr,
29490 Expr::Binary {
29491 op: BinOp::CosineDistance,
29492 ..
29493 }
29494 ));
29495 }
29496
29497 #[test]
29498 fn vector_cast_postfix_wraps_string_literal() {
29499 let s = parse("SELECT '[1,2,3]'::vector FROM t");
29500 let Statement::Select(s) = s else { panic!() };
29501 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29502 panic!()
29503 };
29504 assert!(matches!(
29505 expr,
29506 Expr::Cast {
29507 target: CastTarget::Vector,
29508 ..
29509 }
29510 ));
29511 }
29512
29513 #[test]
29514 fn unsupported_cast_target_errors() {
29515 // v7.37.5 ship triage promoted the parser to accept every
29516 // ident as a `CastTarget::Named(canonical)`; the engine
29517 // surfaces the "unsupported cast target" error at eval
29518 // time when `type_name_to_data_type` can't resolve it.
29519 // Parser-side error now requires a NON-ident after `::`
29520 // (e.g. a punctuation token).
29521 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29522 assert_eq!(err.message, "syntax error at or near \",\"");
29523 }
29524
29525 #[test]
29526 fn tx_statements_round_trip() {
29527 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29528 let original = parse(q);
29529 let again = parse_statement(&original.to_string()).unwrap();
29530 assert_eq!(original, again);
29531 }
29532 }
29533
29534 #[test]
29535 fn interval_text_parsing_units() {
29536 // v7.37.5 β — three-field shape `(months, days, micros)` so
29537 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29538 // Single unit.
29539 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29540 assert_eq!(
29541 parse_interval_text("24 hours"),
29542 Some((0, 0, 86_400_000_000))
29543 );
29544 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29545 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29546 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29547 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29548 // Compound spans accumulate per-dimension.
29549 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29550 assert_eq!(
29551 parse_interval_text("1 day 2 hours"),
29552 Some((0, 1, 7_200_000_000))
29553 );
29554 // Negative numbers carry through per-dimension.
29555 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29556 // Bad shapes return None.
29557 assert_eq!(parse_interval_text(""), None);
29558 assert_eq!(parse_interval_text("garbage"), None);
29559 assert_eq!(parse_interval_text("1 fortnight"), None);
29560 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29561 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29562 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29563 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29564 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29565 }
29566
29567 #[test]
29568 fn interval_literal_roundtrips_via_display() {
29569 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29570 let s = parsed.to_string();
29571 // Display preserves the original text verbatim.
29572 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29573 // And re-parsing yields a structurally equal statement.
29574 let again = parse_statement(&s).unwrap();
29575 assert_eq!(parsed, again);
29576 }
29577
29578 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29579
29580 #[test]
29581 fn parser_recognises_create_publication_bare() {
29582 let s = parse("CREATE PUBLICATION pub_a");
29583 let Statement::CreatePublication(p) = s else {
29584 panic!("expected CreatePublication, got {s:?}")
29585 };
29586 assert_eq!(p.name, "pub_a");
29587 assert_eq!(p.scope, PublicationScope::AllTables);
29588 }
29589
29590 #[test]
29591 fn parser_recognises_create_publication_for_all_tables() {
29592 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29593 let Statement::CreatePublication(p) = s else {
29594 panic!("expected CreatePublication, got {s:?}")
29595 };
29596 assert_eq!(p.name, "pub_a");
29597 assert_eq!(p.scope, PublicationScope::AllTables);
29598 }
29599
29600 #[test]
29601 fn parser_recognises_drop_publication() {
29602 let s = parse("DROP PUBLICATION pub_a");
29603 let Statement::DropPublication { name, .. } = s else {
29604 panic!("expected DropPublication, got {s:?}")
29605 };
29606 assert_eq!(name, "pub_a");
29607 }
29608
29609 #[test]
29610 fn parser_recognises_for_table_list() {
29611 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29612 let Statement::CreatePublication(p) = s else {
29613 panic!("expected CreatePublication, got {s:?}")
29614 };
29615 assert_eq!(p.name, "pub_a");
29616 let PublicationScope::ForTables(ts) = p.scope else {
29617 panic!("expected ForTables scope")
29618 };
29619 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29620 }
29621
29622 #[test]
29623 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29624 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29625 // is rejected (`invalid publication object list`; the old
29626 // test pinned an unverifiable "PG 19 accepts both" claim);
29627 // TABLES pairs with IN SCHEMA.
29628 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29629 .expect_err("bare FOR TABLES must reject");
29630 assert!(
29631 alloc::format!("{err}").contains("invalid publication object list"),
29632 "got: {err}"
29633 );
29634 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29635 let Statement::CreatePublication(p) = s else {
29636 panic!("expected CreatePublication, got {s:?}")
29637 };
29638 let PublicationScope::TablesInSchema(schema) = p.scope else {
29639 panic!("expected TablesInSchema")
29640 };
29641 assert_eq!(schema, "public");
29642 }
29643
29644 #[test]
29645 fn parser_recognises_for_all_tables_except_list() {
29646 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29647 let Statement::CreatePublication(p) = s else {
29648 panic!()
29649 };
29650 let PublicationScope::AllTablesExcept(ts) = p.scope else {
29651 panic!("expected AllTablesExcept")
29652 };
29653 assert_eq!(ts, alloc::vec!["t1", "t2"]);
29654 }
29655
29656 #[test]
29657 fn parser_rejects_for_table_with_empty_list() {
29658 // `FOR TABLE` with nothing after is a parse error.
29659 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29660 .expect_err("must error on empty list");
29661 // No specific message asserted — the call falls through to
29662 // expect_ident_like which yields "expected identifier, got …".
29663 assert!(!err.message.is_empty());
29664 }
29665
29666 #[test]
29667 fn parser_recognises_show_publications() {
29668 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29669 // bare ident in this position, NOT a reserved keyword.
29670 let s = parse("SHOW PUBLICATIONS");
29671 assert!(matches!(s, Statement::ShowPublications));
29672 }
29673
29674 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29675
29676 #[test]
29677 fn parser_recognises_create_subscription_single_publication() {
29678 let s = parse(
29679 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29680 );
29681 let Statement::CreateSubscription(c) = s else {
29682 panic!("expected CreateSubscription, got {s:?}")
29683 };
29684 assert_eq!(c.name, "sub_a");
29685 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29686 assert_eq!(c.publications, alloc::vec!["pub_a"]);
29687 }
29688
29689 #[test]
29690 fn parser_recognises_create_subscription_multi_publication() {
29691 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29692 let Statement::CreateSubscription(c) = s else {
29693 panic!()
29694 };
29695 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29696 }
29697
29698 #[test]
29699 fn parser_rejects_create_subscription_missing_connection() {
29700 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29701 .expect_err("must error on missing CONNECTION");
29702 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29703 }
29704
29705 #[test]
29706 fn parser_rejects_create_subscription_missing_publication() {
29707 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29708 .expect_err("must error on missing PUBLICATION");
29709 assert_eq!(err.message, "syntax error at end of input");
29710 }
29711
29712 #[test]
29713 fn parser_recognises_drop_subscription() {
29714 let s = parse("DROP SUBSCRIPTION sub_a");
29715 let Statement::DropSubscription { name, .. } = s else {
29716 panic!("expected DropSubscription, got {s:?}")
29717 };
29718 assert_eq!(name, "sub_a");
29719 }
29720
29721 #[test]
29722 fn parser_recognises_show_subscriptions() {
29723 let s = parse("SHOW SUBSCRIPTIONS");
29724 assert!(matches!(s, Statement::ShowSubscriptions));
29725 }
29726
29727 #[test]
29728 fn parser_recognises_wait_for_wal_position_no_timeout() {
29729 let s = parse("WAIT FOR WAL POSITION 12345");
29730 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29731 panic!("expected WaitForWalPosition, got {s:?}")
29732 };
29733 assert_eq!(pos, 12345);
29734 assert!(timeout_ms.is_none());
29735 }
29736
29737 #[test]
29738 fn parser_recognises_wait_for_wal_position_with_timeout() {
29739 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29740 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29741 panic!()
29742 };
29743 assert_eq!(pos, 67890);
29744 assert_eq!(timeout_ms, Some(5000));
29745 }
29746
29747 #[test]
29748 fn parser_rejects_wait_with_negative_position() {
29749 // The lexer treats `-` as a token; `expect_u64_literal`
29750 // only sees the Integer that follows, so the negative
29751 // arrives as a unary-minus expression at higher levels.
29752 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29753 // parse error one way or another.
29754 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29755 assert!(!err.message.is_empty());
29756 }
29757
29758 #[test]
29759 fn parser_recognises_bare_analyze() {
29760 let s = parse("ANALYZE");
29761 assert!(matches!(s, Statement::Analyze(None)));
29762 }
29763
29764 #[test]
29765 fn parser_recognises_analyze_with_table() {
29766 let s = parse("ANALYZE users");
29767 let Statement::Analyze(Some(name)) = s else {
29768 panic!("expected Analyze, got {s:?}")
29769 };
29770 assert_eq!(name, "users");
29771 }
29772
29773 #[test]
29774 fn parser_recognises_analyze_with_quoted_table() {
29775 let s = parse("ANALYZE \"Mixed Case\"");
29776 let Statement::Analyze(Some(name)) = s else {
29777 panic!()
29778 };
29779 assert_eq!(name, "Mixed Case");
29780 }
29781
29782 #[test]
29783 fn parser_rejects_analyze_with_garbage_token() {
29784 let err = parse_statement("ANALYZE 42").expect_err("must error");
29785 assert!(!err.message.is_empty());
29786 }
29787
29788 #[test]
29789 fn analyze_display_roundtrips() {
29790 for sql in ["ANALYZE", "ANALYZE users"] {
29791 let s = parse(sql);
29792 let printed = s.to_string();
29793 let again = parse_statement(&printed)
29794 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29795 assert_eq!(s, again);
29796 }
29797 }
29798
29799 #[test]
29800 fn wait_for_display_roundtrips() {
29801 for sql in [
29802 "WAIT FOR WAL POSITION 12345",
29803 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29804 ] {
29805 let s = parse(sql);
29806 let printed = s.to_string();
29807 let again = parse_statement(&printed)
29808 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29809 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29810 }
29811 }
29812
29813 #[test]
29814 fn subscription_ddl_display_roundtrips() {
29815 for sql in [
29816 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29817 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29818 "DROP SUBSCRIPTION sub_a",
29819 "SHOW SUBSCRIPTIONS",
29820 ] {
29821 let s = parse(sql);
29822 let printed = s.to_string();
29823 let again = parse_statement(&printed)
29824 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29825 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29826 }
29827 }
29828
29829 #[test]
29830 fn parser_drop_dispatches_user_vs_publication() {
29831 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29832 // tokenises DROP. Both targets must still parse.
29833 let s = parse("DROP USER 'alice'");
29834 let Statement::DropUser { name, .. } = s else {
29835 panic!("expected DropUser, got {s:?}")
29836 };
29837 assert_eq!(name, "alice");
29838 // And DROP PUBLICATION lands the new variant.
29839 let s = parse("DROP PUBLICATION p1");
29840 assert!(matches!(s, Statement::DropPublication { .. }));
29841 }
29842
29843 #[test]
29844 fn publication_ddl_display_roundtrips() {
29845 // Every CREATE PUBLICATION variant must Display → parse →
29846 // same AST. v6.1.3 covers all three scope shapes.
29847 for sql in [
29848 "CREATE PUBLICATION pub_a",
29849 "CREATE PUBLICATION pub_a FOR ALL TABLES",
29850 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29851 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29852 "DROP PUBLICATION pub_a",
29853 "SHOW PUBLICATIONS",
29854 ] {
29855 let s = parse(sql);
29856 let printed = s.to_string();
29857 let again = parse_statement(&printed)
29858 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29859 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29860 }
29861 }
29862
29863 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29864
29865 #[test]
29866 fn create_function_returns_trigger_plpgsql_minimal() {
29867 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29868 let s = parse(sql);
29869 let Statement::CreateFunction(f) = s else {
29870 panic!("expected CreateFunction");
29871 };
29872 assert_eq!(f.name, "noop");
29873 assert!(!f.or_replace);
29874 assert!(f.args.is_empty());
29875 assert!(matches!(f.returns, FunctionReturn::Trigger));
29876 assert_eq!(f.language, "plpgsql");
29877 let FunctionBody::PlPgSql(block) = f.body else {
29878 panic!("expected PlPgSql body");
29879 };
29880 assert_eq!(block.statements.len(), 1);
29881 assert!(matches!(
29882 block.statements[0],
29883 PlPgSqlStmt::Return(ReturnTarget::New)
29884 ));
29885 }
29886
29887 #[test]
29888 fn create_function_or_replace_with_assignment() {
29889 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29890 // RETURN NEW.
29891 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29892BEGIN
29893 NEW.search_vector := to_tsvector('english', NEW.subject);
29894 RETURN NEW;
29895END;
29896$$";
29897 let s = parse(sql);
29898 let Statement::CreateFunction(f) = s else {
29899 panic!("expected CreateFunction");
29900 };
29901 assert!(f.or_replace);
29902 let FunctionBody::PlPgSql(block) = &f.body else {
29903 panic!("expected PlPgSql body");
29904 };
29905 assert_eq!(block.statements.len(), 2);
29906 // First statement: NEW.search_vector := to_tsvector(...)
29907 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29908 panic!("expected Assign as first stmt");
29909 };
29910 match target {
29911 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29912 other => panic!("expected NEW.col, got {other:?}"),
29913 }
29914 // Second statement: RETURN NEW
29915 assert!(matches!(
29916 block.statements[1],
29917 PlPgSqlStmt::Return(ReturnTarget::New)
29918 ));
29919 }
29920
29921 #[test]
29922 fn create_trigger_after_insert_or_update() {
29923 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29924 let s = parse(sql);
29925 let Statement::CreateTrigger(t) = s else {
29926 panic!("expected CreateTrigger");
29927 };
29928 assert_eq!(t.name, "tg");
29929 assert_eq!(t.table, "messages");
29930 assert_eq!(t.timing, TriggerTiming::After);
29931 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29932 assert_eq!(t.for_each, TriggerForEach::Row);
29933 assert_eq!(t.function, "update_sv");
29934 }
29935
29936 #[test]
29937 fn create_trigger_before_delete_execute_procedure_alias() {
29938 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29939 let sql =
29940 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29941 let s = parse(sql);
29942 let Statement::CreateTrigger(t) = s else {
29943 panic!("expected CreateTrigger");
29944 };
29945 assert_eq!(t.timing, TriggerTiming::Before);
29946 assert_eq!(t.events, vec![TriggerEvent::Delete]);
29947 }
29948
29949 #[test]
29950 fn drop_trigger_if_exists_round_trips() {
29951 // No parser support for DROP TRIGGER yet — added in v7.12.5
29952 // alongside the broader DROP …{IF EXISTS} cleanup. The
29953 // AST + Display impls are in place so we round-trip via
29954 // construction:
29955 let s = Statement::DropTrigger {
29956 name: "tg".into(),
29957 table: "messages".into(),
29958 if_exists: true,
29959 };
29960 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29961 }
29962
29963 #[test]
29964 fn trigger_ddl_display_roundtrips_through_parser() {
29965 // CREATE TRIGGER + its referenced CREATE FUNCTION must
29966 // Display → parse → same AST (modulo PL/pgSQL body
29967 // formatting which is parser-canonicalised).
29968 for sql in [
29969 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29970 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29971 ] {
29972 let s = parse(sql);
29973 let printed = s.to_string();
29974 let again = parse_statement(&printed)
29975 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29976 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29977 }
29978 }
29979}