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 = self.consume_if_exists();
3208 let name = self.expect_ident_like()?;
3209 if matches!(
3210 self.peek(),
3211 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3212 || s.eq_ignore_ascii_case("restrict")
3213 ) {
3214 self.advance();
3215 }
3216 Ok(Statement::DropIndex { name, if_exists })
3217 }
3218 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3219 // [CASCADE|RESTRICT]. SPG is single-database;
3220 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3221 // name [, name…] [CASCADE | RESTRICT]. Real
3222 // unregister (was silent no-op pre-v7.17).
3223 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3224 self.advance();
3225 let if_exists = self.consume_if_exists();
3226 let mut names = vec![self.expect_ident_like()?];
3227 while matches!(self.peek(), Token::Comma) {
3228 self.advance();
3229 names.push(self.expect_ident_like()?);
3230 }
3231 if matches!(
3232 self.peek(),
3233 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3234 || s.eq_ignore_ascii_case("restrict")
3235 ) {
3236 self.advance();
3237 }
3238 Ok(Statement::DropSchema { names, if_exists })
3239 }
3240 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3241 // name [, name…] [CASCADE|RESTRICT].
3242 Token::Ident(s) | Token::QuotedIdent(s)
3243 if s.eq_ignore_ascii_case("type") =>
3244 {
3245 self.advance();
3246 let if_exists = self.consume_if_exists();
3247 let mut names = vec![self.expect_ident_like()?];
3248 while matches!(self.peek(), Token::Comma) {
3249 self.advance();
3250 names.push(self.expect_ident_like()?);
3251 }
3252 if matches!(
3253 self.peek(),
3254 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3255 || s.eq_ignore_ascii_case("restrict")
3256 ) {
3257 self.advance();
3258 }
3259 Ok(Statement::DropType { names, if_exists })
3260 }
3261 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3262 // name [, name…] [CASCADE|RESTRICT].
3263 Token::Ident(s) | Token::QuotedIdent(s)
3264 if s.eq_ignore_ascii_case("domain") =>
3265 {
3266 self.advance();
3267 let if_exists = self.consume_if_exists();
3268 let mut names = vec![self.expect_ident_like()?];
3269 while matches!(self.peek(), Token::Comma) {
3270 self.advance();
3271 names.push(self.expect_ident_like()?);
3272 }
3273 if matches!(
3274 self.peek(),
3275 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3276 || s.eq_ignore_ascii_case("restrict")
3277 ) {
3278 self.advance();
3279 }
3280 Ok(Statement::DropDomain { names, if_exists })
3281 }
3282 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3283 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3284 Token::Ident(s) | Token::QuotedIdent(s)
3285 if s.eq_ignore_ascii_case("materialized") =>
3286 {
3287 self.advance();
3288 let nxt = self.peek().clone();
3289 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3290 {
3291 return Err(self.err(alloc::format!(
3292 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3293 )));
3294 }
3295 self.advance();
3296 let if_exists = self.consume_if_exists();
3297 let mut names = vec![self.expect_ident_like()?];
3298 while matches!(self.peek(), Token::Comma) {
3299 self.advance();
3300 names.push(self.expect_ident_like()?);
3301 }
3302 if matches!(
3303 self.peek(),
3304 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3305 || s.eq_ignore_ascii_case("restrict")
3306 ) {
3307 self.advance();
3308 }
3309 Ok(Statement::DropMaterializedView { names, if_exists })
3310 }
3311 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3312 // name [, name…] [CASCADE|RESTRICT].
3313 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3314 self.advance();
3315 let if_exists = self.consume_if_exists();
3316 let mut names = vec![self.expect_ident_like()?];
3317 while matches!(self.peek(), Token::Comma) {
3318 self.advance();
3319 names.push(self.expect_ident_like()?);
3320 }
3321 if matches!(
3322 self.peek(),
3323 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3324 || s.eq_ignore_ascii_case("restrict")
3325 ) {
3326 self.advance();
3327 }
3328 Ok(Statement::DropView { names, if_exists })
3329 }
3330 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3331 // [CASCADE|RESTRICT]. Real removal from catalog
3332 // (was a silent no-op pre-v7.17).
3333 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3334 self.advance();
3335 let if_exists = self.consume_if_exists();
3336 let mut names = vec![self.expect_ident_like()?];
3337 while matches!(self.peek(), Token::Comma) {
3338 self.advance();
3339 names.push(self.expect_ident_like()?);
3340 }
3341 if matches!(
3342 self.peek(),
3343 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3344 || s.eq_ignore_ascii_case("restrict")
3345 ) {
3346 self.advance();
3347 }
3348 Ok(Statement::DropSequence { names, if_exists })
3349 }
3350 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3351 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3352 self.advance();
3353 self.parse_drop_policy_after_keyword()
3354 }
3355 // v7.37.17 (17.6 siblings) — DROP <target> for
3356 // targets SPG doesn't natively track. pg_dump
3357 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3358 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3359 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3360 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3361 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3362 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3363 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3364 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3365 // etc. — accept + Empty-return so pg_dump tails
3366 // load through. Materialized-view drop dispatches
3367 // to the existing DropTable path when the token
3368 // is Materialized-View-shaped (elsewhere in
3369 // this parser).
3370 Token::Ident(s) | Token::QuotedIdent(s)
3371 if s.eq_ignore_ascii_case("text")
3372 // The DROP dispatch matches on PEEK — `text` is
3373 // not yet consumed, so SEARCH/CONFIGURATION sit
3374 // at pos+1/pos+2 (the round-695 trap's mirror).
3375 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3376 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3377 {
3378 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3379 // validates the name; DICTIONARY / PARSER / TEMPLATE
3380 // stay in the noise arm below.
3381 self.advance(); // TEXT
3382 self.advance(); // SEARCH
3383 self.advance(); // CONFIGURATION
3384 let if_exists = self.consume_if_exists();
3385 let names = self.take_comma_separated_names();
3386 self.consume_until_statement_boundary();
3387 if if_exists {
3388 return Ok(Statement::Empty);
3389 }
3390 Ok(Statement::ValidateOnly {
3391 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3392 names,
3393 })
3394 }
3395 Token::Ident(s) | Token::QuotedIdent(s)
3396 if matches!(
3397 s.to_ascii_lowercase().as_str(),
3398 "type"
3399 | "domain"
3400 | "operator"
3401 | "cast"
3402 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3403 // TEMPLATE (CONFIGURATION intercepted above).
3404 | "text"
3405 | "materialized"
3406 | "large"
3407 | "role"
3408 | "access"
3409 | "procedure"
3410 | "routine"
3411 ) =>
3412 {
3413 self.consume_until_statement_boundary();
3414 Ok(Statement::Empty)
3415 }
3416 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3417 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3418 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3419 // foreign-data warning family (round 706) so a
3420 // CREATE→DROP sequence in a dump stays consistent.
3421 Token::Ident(s) | Token::QuotedIdent(s)
3422 if s.eq_ignore_ascii_case("server")
3423 || s.eq_ignore_ascii_case("foreign") =>
3424 {
3425 self.advance();
3426 self.consume_until_statement_boundary();
3427 Ok(Statement::ValidateOnly {
3428 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3429 names: Vec::new(),
3430 })
3431 }
3432 Token::Ident(s) | Token::QuotedIdent(s)
3433 if s.eq_ignore_ascii_case("collation")
3434 || s.eq_ignore_ascii_case("tablespace") =>
3435 {
3436 let kind = if s.eq_ignore_ascii_case("collation") {
3437 crate::ast::ValidateOnlyKind::CollationName
3438 } else {
3439 crate::ast::ValidateOnlyKind::TablespaceName
3440 };
3441 self.advance();
3442 let if_exists = self.consume_if_exists();
3443 let names = self.take_comma_separated_names();
3444 self.consume_until_statement_boundary();
3445 if if_exists {
3446 return Ok(Statement::Empty);
3447 }
3448 Ok(Statement::ValidateOnly { kind, names })
3449 }
3450 Token::Ident(s) | Token::QuotedIdent(s)
3451 if s.eq_ignore_ascii_case("event") =>
3452 {
3453 self.advance();
3454 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3455 {
3456 self.advance();
3457 }
3458 let if_exists = self.consume_if_exists();
3459 let names = self.take_comma_separated_names();
3460 self.consume_until_statement_boundary();
3461 if if_exists {
3462 return Ok(Statement::Empty);
3463 }
3464 Ok(Statement::ValidateOnly {
3465 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3466 names,
3467 })
3468 }
3469 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3470 // leave the noise list; see the ValidateOnly kinds.
3471 Token::Ident(s) | Token::QuotedIdent(s)
3472 if s.eq_ignore_ascii_case("conversion")
3473 || s.eq_ignore_ascii_case("language")
3474 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3475 // FIRST — the first draft looked for it after.
3476 || s.eq_ignore_ascii_case("procedural") =>
3477 {
3478 let kind = if s.eq_ignore_ascii_case("conversion") {
3479 crate::ast::ValidateOnlyKind::ConversionName
3480 } else {
3481 crate::ast::ValidateOnlyKind::LanguageName
3482 };
3483 self.advance();
3484 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3485 {
3486 self.advance();
3487 }
3488 let if_exists = self.consume_if_exists();
3489 let names = self.take_comma_separated_names();
3490 self.consume_until_statement_boundary();
3491 if if_exists {
3492 return Ok(Statement::Empty);
3493 }
3494 Ok(Statement::ValidateOnly { kind, names })
3495 }
3496 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3497 // name(argtypes)[, …]`. Parsed for real so the engine
3498 // can answer as PG does; see Statement::DropAggregate.
3499 Token::Ident(s) | Token::QuotedIdent(s)
3500 if s.eq_ignore_ascii_case("aggregate") =>
3501 {
3502 self.advance();
3503 let if_exists = self.consume_if_exists();
3504 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3505 loop {
3506 let name = self.expect_ident_like()?;
3507 if !matches!(self.peek(), Token::LParen) {
3508 return Err(self.err(alloc::format!(
3509 "expected argument list after DROP AGGREGATE {name}"
3510 )));
3511 }
3512 self.advance();
3513 let mut args: Vec<String> = Vec::new();
3514 let mut star = false;
3515 loop {
3516 match self.peek().clone() {
3517 Token::RParen => {
3518 self.advance();
3519 break;
3520 }
3521 Token::Star => {
3522 self.advance();
3523 star = true;
3524 }
3525 Token::Comma => {
3526 self.advance();
3527 }
3528 _ => {
3529 // A type name may be multi-token
3530 // (`double precision`); glue idents
3531 // until , or ).
3532 let mut t = self.expect_ident_like()?;
3533 while let Token::Ident(nx) = self.peek() {
3534 let nx = nx.clone();
3535 self.advance();
3536 t.push(' ');
3537 t.push_str(&nx);
3538 }
3539 args.push(t);
3540 }
3541 }
3542 }
3543 items.push((name, if star { None } else { Some(args) }));
3544 if matches!(self.peek(), Token::Comma) {
3545 self.advance();
3546 } else {
3547 break;
3548 }
3549 }
3550 self.consume_until_statement_boundary();
3551 Ok(Statement::DropAggregate { if_exists, items })
3552 }
3553 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3554 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3555 // installed; `IF EXISTS` is the spelling that says do
3556 // not, and it keeps the no-op.
3557 Token::Ident(s) | Token::QuotedIdent(s)
3558 if s.eq_ignore_ascii_case("extension") =>
3559 {
3560 self.advance();
3561 let if_exists = self.consume_if_exists();
3562 let names = self.take_comma_separated_names();
3563 self.consume_until_statement_boundary();
3564 if if_exists {
3565 return Ok(Statement::Empty);
3566 }
3567 Ok(Statement::ValidateOnly {
3568 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3569 names,
3570 })
3571 }
3572 Token::Ident(s) | Token::QuotedIdent(s)
3573 if s.eq_ignore_ascii_case("statistics") =>
3574 {
3575 self.parse_drop_statistics_after_drop()
3576 }
3577 other => Err(self.err(format!(
3578 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3579 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3580 ))),
3581 }
3582 }
3583 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3584 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3585 // and accepted before the view name. SPG materialised
3586 // views re-evaluate on read (always-fresh semantics), so
3587 // the CONCURRENTLY-vs-serial distinction has no runtime
3588 // effect — the refresh body does not block readers either
3589 // way. Same accept-and-no-op pattern as DETACH PARTITION
3590 // CONCURRENTLY (16.5).
3591 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3592 self.advance();
3593 let nxt = self.peek().clone();
3594 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3595 {
3596 return Err(self.err(alloc::format!(
3597 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3598 )));
3599 }
3600 self.advance();
3601 let nxt2 = self.peek().clone();
3602 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3603 {
3604 return Err(self.err(alloc::format!(
3605 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3606 )));
3607 }
3608 self.advance();
3609 // Optional CONCURRENTLY noise word — consumed without
3610 // changing semantics.
3611 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3612 {
3613 self.advance();
3614 }
3615 let name = self.expect_ident_like()?;
3616 let with_data = self.parse_optional_with_data(true)?;
3617 Ok(Statement::RefreshMaterializedView { name, with_data })
3618 }
3619 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3620 self.advance();
3621 self.parse_update_after_keyword()
3622 }
3623 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3624 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3625 // [CASCADE | RESTRICT]. Clears every row from each named
3626 // table. Parses at the top level; the engine dispatcher
3627 // walks Statement::Truncate.
3628 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3629 self.advance();
3630 // Optional TABLE noise word — PG accepts both the reserved
3631 // token and the bare identifier spelling.
3632 if matches!(self.peek(), Token::Table)
3633 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3634 {
3635 self.advance();
3636 }
3637 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3638 // not absorbed. The lookahead keeps a table genuinely
3639 // called `only` working: the keyword is a keyword only
3640 // when a name follows it.
3641 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3642 if s.eq_ignore_ascii_case("only"))
3643 && matches!(
3644 self.tokens.get(self.pos + 1),
3645 Some(Token::Ident(_) | Token::QuotedIdent(_))
3646 );
3647 if only {
3648 self.advance();
3649 }
3650 // Table names (comma-separated).
3651 let mut tables = Vec::new();
3652 loop {
3653 tables.push(self.expect_ident_like()?);
3654 if matches!(self.peek(), Token::Comma) {
3655 self.advance();
3656 continue;
3657 }
3658 break;
3659 }
3660 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3661 let mut restart_identity = false;
3662 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3663 {
3664 self.advance();
3665 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3666 {
3667 self.advance();
3668 restart_identity = true;
3669 }
3670 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3671 {
3672 self.advance();
3673 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3674 {
3675 self.advance();
3676 }
3677 }
3678 // Optional CASCADE / RESTRICT.
3679 let mut cascade = false;
3680 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3681 {
3682 self.advance();
3683 cascade = true;
3684 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3685 {
3686 self.advance();
3687 }
3688 Ok(Statement::Truncate {
3689 tables,
3690 restart_identity,
3691 cascade,
3692 only,
3693 })
3694 }
3695 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3696 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3697 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3698 // rows change so the index tree is always up-to-date;
3699 // REINDEX is a strict no-op. Accept the whole statement
3700 // shape to boundary for pg_dump round-trip compatibility.
3701 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3702 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3703 // index bloat to rebuild, so the work stays a no-op, but PG
3704 // validates what it was pointed at and this swallowed the
3705 // name at parse time — `REINDEX TABLE typo` reported
3706 // success. Measured on PG18: INDEX / TABLE name a relation,
3707 // SCHEMA a schema, SYSTEM nothing.
3708 self.advance();
3709 self.parse_reindex_tail()
3710 }
3711 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3712 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3713 // SPG has no MVCC bloat today (Phase D visibility map
3714 // queues with v7.38); the freezer collapses hot-tier
3715 // rows into cold segments automatically. VACUUM is a
3716 // no-op — pg_dump maintenance scripts and Discourse's
3717 // periodic-maintenance path both emit it.
3718 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3719 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3720 // actual bloat, so the pre-MVCC accept-and-ignore posture
3721 // became a silent no-op on a customer's manual reclaim.
3722 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3723 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3724 // ANALYZE is captured, the optional table name is captured.
3725 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3726 self.advance();
3727 // Parenthesised option list: absorb it.
3728 if matches!(self.peek(), Token::LParen) {
3729 let mut depth = 0usize;
3730 loop {
3731 match self.advance() {
3732 Token::LParen => depth += 1,
3733 Token::RParen => {
3734 depth -= 1;
3735 if depth == 0 {
3736 break;
3737 }
3738 }
3739 Token::Eof => break,
3740 _ => {}
3741 }
3742 }
3743 }
3744 let mut analyze = false;
3745 let mut table: Option<String> = None;
3746 loop {
3747 match self.peek() {
3748 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3749 // an identifier, so the loop below broke out on it and
3750 // dropped the table name: `VACUUM FULL nosuch` was
3751 // accepted where `VACUUM nosuch` was refused.
3752 Token::Full => {
3753 self.advance();
3754 }
3755 Token::Ident(w) | Token::QuotedIdent(w) => {
3756 let wl = w.to_ascii_lowercase();
3757 match wl.as_str() {
3758 "full" | "freeze" | "verbose" => {
3759 self.advance();
3760 }
3761 "analyze" | "analyse" => {
3762 analyze = true;
3763 self.advance();
3764 }
3765 _ => {
3766 table = Some(self.expect_ident_like()?);
3767 break;
3768 }
3769 }
3770 }
3771 _ => break,
3772 }
3773 }
3774 // Optional trailing column list / anything else to the
3775 // statement boundary (PG accepts per-column ANALYZE).
3776 self.consume_until_statement_boundary();
3777 Ok(Statement::Vacuum { table, analyze })
3778 }
3779 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3780 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3781 // <index>. PG stores rows in physical order matching
3782 // an index; SPG's hot-tier is append-only + cold-tier
3783 // is segment-frozen, so clustering has no persistent
3784 // effect. Accept-and-no-op for pg_dump compat.
3785 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3786 // v7.39 (round 535) — same as REINDEX above: the relation is
3787 // carried so the engine can refuse one that does not exist.
3788 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3789 self.advance();
3790 self.parse_cluster_tail()
3791 }
3792 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3793 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3794 // optional string payload; UNLISTEN takes a channel or `*`.
3795 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3796 self.advance();
3797 let ch = match self.advance() {
3798 Token::Ident(c) | Token::QuotedIdent(c) => c,
3799 other => {
3800 return Err(self.err(format!(
3801 "expected channel name after LISTEN, got {other:?}"
3802 )));
3803 }
3804 };
3805 Ok(Statement::Listen(ch))
3806 }
3807 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3808 self.advance();
3809 let channel = match self.advance() {
3810 Token::Ident(c) | Token::QuotedIdent(c) => c,
3811 other => {
3812 return Err(self.err(format!(
3813 "expected channel name after NOTIFY, got {other:?}"
3814 )));
3815 }
3816 };
3817 let payload = if matches!(self.peek(), Token::Comma) {
3818 self.advance();
3819 match self.advance() {
3820 Token::String(p) => Some(p),
3821 other => {
3822 return Err(self.err(format!(
3823 "expected string payload after NOTIFY <channel>, got {other:?}"
3824 )));
3825 }
3826 }
3827 } else {
3828 None
3829 };
3830 Ok(Statement::Notify { channel, payload })
3831 }
3832 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3833 self.advance();
3834 match self.advance() {
3835 Token::Star => Ok(Statement::Unlisten(None)),
3836 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3837 other => Err(self.err(format!(
3838 "expected channel name or * after UNLISTEN, got {other:?}"
3839 ))),
3840 }
3841 }
3842 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3843 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3844 // process-wide write lock today; explicit LOCK has no
3845 // effect. Accept-and-no-op for pg_dump / migration
3846 // compat.
3847 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3848 self.advance();
3849 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3850 // engine holds a process-wide write lock), but the TABLE
3851 // NAME is now carried out so the engine can refuse one that
3852 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3853 // READ|WRITE` is a different statement with the same first
3854 // word; it keeps the old no-op, because a MySQL dump's
3855 // bracket names tables it is about to create.
3856 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3857 if k.eq_ignore_ascii_case("tables"));
3858 if mysql_tables {
3859 self.consume_until_statement_boundary();
3860 return Ok(Statement::Empty);
3861 }
3862 if matches!(self.peek(), Token::Table) {
3863 self.advance();
3864 }
3865 let names = self.take_comma_separated_names();
3866 self.consume_until_statement_boundary();
3867 Ok(Statement::ValidateOnly {
3868 kind: crate::ast::ValidateOnlyKind::LockTable,
3869 names,
3870 })
3871 }
3872 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3873 // durability marker + snapshot in PG. SPG has WAL
3874 // checkpointing on a byte / time schedule (v7.37.10
3875 // 60s / 4 MiB defaults). The bare statement parses to
3876 // `Statement::Empty` here (the no_std engine owns no
3877 // WAL / snapshot); v7.37 Epic Du wires the HOST
3878 // (embedded `Database::execute_buffered`, via
3879 // `sql_is_checkpoint`) to force an immediate synchronous
3880 // checkpoint through `Database::checkpoint` — a real
3881 // durability barrier, matching PG.
3882 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3883 self.advance();
3884 self.consume_until_statement_boundary();
3885 Ok(Statement::Empty)
3886 }
3887 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3888 self.advance();
3889 self.parse_delete_after_keyword()
3890 }
3891 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3892 // ALTER is not a reserved keyword in the lexer — handled
3893 // as a bare ident here.
3894 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3895 self.advance();
3896 self.parse_alter_after_keyword()
3897 }
3898 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3899 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3900 // additions needed.
3901 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3902 self.advance();
3903 self.parse_wait_after_keyword()
3904 }
3905 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3906 // Bare ANALYZE → analyse every user table; ANALYZE
3907 // <name> → re-stats one. The argument is an optional
3908 // ident (or quoted ident); anything else is a parse
3909 // error.
3910 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3911 // `WHERE` filter (carved out per V6_7_DESIGN.md
3912 // STABILITY). Lex order: identifier "compact" → "cold"
3913 // → "segments". Anything else after `COMPACT` is a
3914 // parse error.
3915 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3916 self.advance();
3917 let next = self.peek().clone();
3918 let cold = match next {
3919 Token::Ident(s) | Token::QuotedIdent(s) => s,
3920 _ => {
3921 return Err(
3922 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3923 );
3924 }
3925 };
3926 if !cold.eq_ignore_ascii_case("cold") {
3927 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3928 }
3929 self.advance();
3930 let next = self.peek().clone();
3931 let segments = match next {
3932 Token::Ident(s) | Token::QuotedIdent(s) => s,
3933 _ => {
3934 return Err(self.err(format!(
3935 "expected SEGMENTS after COMPACT COLD, got {:?}",
3936 self.peek()
3937 )));
3938 }
3939 };
3940 if !segments.eq_ignore_ascii_case("segments") {
3941 return Err(self.err(format!(
3942 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3943 )));
3944 }
3945 self.advance();
3946 Ok(Statement::CompactColdSegments)
3947 }
3948 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3949 // Parsed as a case-insensitive identifier since MERGE
3950 // isn't a reserved lexer keyword (collides with the
3951 // mysqldump `ALGORITHM = MERGE` view clause if it
3952 // were); the inner parser drives the rest of the
3953 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3954 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3955 self.advance();
3956 self.parse_merge_after_keyword()
3957 }
3958 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3959 self.advance();
3960 let target = match self.peek() {
3961 Token::Eof | Token::Semicolon => None,
3962 Token::Ident(_) | Token::QuotedIdent(_) => {
3963 Some(self.expect_ident_like()?)
3964 }
3965 other => {
3966 return Err(self.err(format!(
3967 "expected table name or end of statement after ANALYZE, got {other:?}"
3968 )));
3969 }
3970 };
3971 // v7.39 (round 776, F31 J7) — the per-column form
3972 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3973 // here while the VACUUM arm already consumed it; SPG
3974 // analyzes whole tables, so the list parses and is
3975 // accepted like the VACUUM path's.
3976 if target.is_some() && matches!(self.peek(), Token::LParen) {
3977 self.advance();
3978 loop {
3979 let _ = self.expect_ident_like()?;
3980 match self.peek() {
3981 Token::Comma => {
3982 self.advance();
3983 }
3984 Token::RParen => {
3985 self.advance();
3986 break;
3987 }
3988 other => {
3989 return Err(self.err(format!(
3990 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3991 )));
3992 }
3993 }
3994 }
3995 }
3996 Ok(Statement::Analyze(target))
3997 }
3998 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3999 // `default_text_search_config` parameter is consumed
4000 // by the FTS function dispatcher; other parameter
4001 // names are recorded but treated as a no-op so PG
4002 // dump output loads.
4003 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4004 self.advance();
4005 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4006 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4007 // …` which the SessionVar path handles). `LOCAL` is the only
4008 // one that changes semantics — it scopes the change to the
4009 // current transaction — so capture it; SESSION / GLOBAL are
4010 // accepted and treated as the default session scope.
4011 let mut set_local = false;
4012 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4013 let q = s.to_ascii_lowercase();
4014 if q == "local" || q == "session" || q == "global" {
4015 set_local = q == "local";
4016 self.advance();
4017 }
4018 }
4019 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4020 // { DEFAULT | <role> }`. pg_dump's ACL section switches
4021 // to the object owner with it. SPG maps it onto the
4022 // session-role machinery (recorded delta RD-10: PG moves
4023 // session_user too; SPG moves the effective role).
4024 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4025 if s.eq_ignore_ascii_case("authorization"))
4026 {
4027 self.advance(); // AUTHORIZATION
4028 let role = match self.peek().clone() {
4029 Token::Default => {
4030 self.advance();
4031 None
4032 }
4033 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4034 self.advance();
4035 Some(s)
4036 }
4037 _ => None,
4038 };
4039 return Ok(Statement::SetRole(role));
4040 }
4041 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4042 // <collation>]` — change the connection client
4043 // charset. SPG stores UTF-8 always and orders
4044 // bytewise; accept as a no-op.
4045 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4046 {
4047 self.advance();
4048 // v7.39 — this used to parse the clause and throw it
4049 // away ("SPG stores UTF-8 always and orders
4050 // bytewise; accept as a no-op"). That sentence
4051 // stopped being true when collations arrived, and
4052 // once `collation_connection` began driving literal
4053 // comparison, dropping the COLLATE clause became a
4054 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4055 // utf8mb4_general_ci` reported back
4056 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4057 //
4058 // The charset name is emitted as `names` and the
4059 // ENGINE expands it, because which collation a
4060 // charset defaults to is MySQL semantics and belongs
4061 // beside the rest of them, not in the parser.
4062 let mut pairs = alloc::vec::Vec::new();
4063 if matches!(
4064 self.peek(),
4065 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4066 ) {
4067 let charset = match self.advance() {
4068 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4069 _ => unreachable!("peeked an ident-or-string"),
4070 };
4071 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4072 }
4073 // Optional `COLLATE <name>` — emitted AFTER `names`
4074 // so it overrides the charset's default, which is
4075 // what MySQL does.
4076 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4077 {
4078 self.advance();
4079 if matches!(
4080 self.peek(),
4081 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4082 ) {
4083 let coll = match self.advance() {
4084 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4085 _ => unreachable!("peeked an ident-or-string"),
4086 };
4087 pairs.push((
4088 String::from("collation_connection"),
4089 crate::ast::SetValue::Ident(coll),
4090 ));
4091 }
4092 }
4093 if pairs.is_empty() {
4094 return Ok(Statement::Empty);
4095 }
4096 return Ok(Statement::SetParameterList(pairs));
4097 }
4098 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4099 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4100 // uses this to switch to the object owner before
4101 // recreating tables. SPG has no role system so this
4102 // is a no-op.
4103 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4104 {
4105 self.advance(); // ROLE
4106 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4107 // reset to the login identity; a name / string sets the
4108 // effective role that drives current_user + RLS.
4109 let role = match self.peek().clone() {
4110 Token::Default => {
4111 self.advance();
4112 None
4113 }
4114 Token::Ident(s) | Token::QuotedIdent(s)
4115 if s.eq_ignore_ascii_case("none") =>
4116 {
4117 self.advance();
4118 None
4119 }
4120 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4121 self.advance();
4122 Some(s)
4123 }
4124 _ => None,
4125 };
4126 return Ok(Statement::SetRole(role));
4127 }
4128 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4129 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4130 // ISO SQL surface). pg_dump prepends this to fix
4131 // the isolation level for the restore session. SPG
4132 // defaults to READ COMMITTED and doesn't yet honor
4133 // session-set isolation across statements — accept
4134 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4135 // per-tx form is handled elsewhere.
4136 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4137 {
4138 self.advance(); // CHARACTERISTICS
4139 if matches!(self.peek(), Token::As) {
4140 self.advance();
4141 }
4142 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4143 self.advance();
4144 }
4145 // v7.39 — no longer a no-op. The note above said SPG
4146 // "doesn't yet honor session-set isolation across
4147 // statements"; it does now, through
4148 // `default_transaction_isolation`, and measured on
4149 // PG 18.6 this statement is exactly a way to set it:
4150 //
4151 // SET SESSION CHARACTERISTICS AS TRANSACTION
4152 // ISOLATION LEVEL REPEATABLE READ;
4153 // current_setting('default_transaction_isolation')
4154 // -> repeatable read
4155 //
4156 // pg_dump prepends this to fix the level for a
4157 // restore session, so accepting it and doing nothing
4158 // meant the restore ran at a level nobody chose.
4159 //
4160 // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4161 // still consumed and dropped. `default_transaction_read_only`
4162 // exists in the GUC inventory but nothing enforces it,
4163 // and setting a value no code honours is the very
4164 // defect this version is about — a session told it
4165 // holds a guarantee it does not.
4166 let modes = self.parse_isolation_level_clauses()?;
4167 self.consume_until_statement_boundary();
4168 let mut pairs: alloc::vec::Vec<(
4169 alloc::string::String,
4170 crate::ast::SetValue,
4171 )> = alloc::vec::Vec::new();
4172 if let Some(level) = modes.isolation {
4173 pairs.push((
4174 alloc::string::String::from("default_transaction_isolation"),
4175 crate::ast::SetValue::String(alloc::string::String::from(
4176 level.as_pg_str(),
4177 )),
4178 ));
4179 }
4180 if let Some(ro) = modes.read_only {
4181 pairs.push((
4182 alloc::string::String::from("default_transaction_read_only"),
4183 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4184 "on"
4185 } else {
4186 "off"
4187 })),
4188 ));
4189 }
4190 return Ok(if pairs.is_empty() {
4191 Statement::Empty
4192 } else {
4193 Statement::SetParameterList(pairs)
4194 });
4195 }
4196 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4197 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4198 // pg_dump emits this to control the deferrability of
4199 // FK / UNIQUE constraints across a bulk restore. SPG
4200 // has no deferrable-constraint machinery today; the
4201 // FK checker is strict-immediate. Accept-and-no-op
4202 // for pg_dump round-trip compatibility.
4203 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4204 {
4205 self.advance(); // CONSTRAINTS
4206 // v7.39 (round 288) — no longer a no-op: the trailing
4207 // DEFERRED / IMMEDIATE sets the transaction's timing.
4208 // v7.39 (round 308, V29) — and the names are kept.
4209 // They used to be skipped over on the way to the
4210 // DEFERRED keyword, so a named form silently behaved
4211 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4212 // every deferrable constraint in the transaction.
4213 let mut names: alloc::vec::Vec<alloc::string::String> =
4214 alloc::vec::Vec::new();
4215 if matches!(self.peek(), Token::All) {
4216 self.advance();
4217 } else {
4218 loop {
4219 let mut n = self.expect_ident_like()?;
4220 // A schema-qualified name (`public.fk_a`)
4221 // identifies the same constraint; PG resolves
4222 // it by the trailing segment.
4223 while matches!(self.peek(), Token::Dot) {
4224 self.advance();
4225 n = self.expect_ident_like()?;
4226 }
4227 names.push(n);
4228 if matches!(self.peek(), Token::Comma) {
4229 self.advance();
4230 } else {
4231 break;
4232 }
4233 }
4234 }
4235 let deferred = match self.peek() {
4236 Token::Ident(s) | Token::QuotedIdent(s)
4237 if s.eq_ignore_ascii_case("deferred") =>
4238 {
4239 true
4240 }
4241 Token::Ident(s) | Token::QuotedIdent(s)
4242 if s.eq_ignore_ascii_case("immediate") =>
4243 {
4244 false
4245 }
4246 other => {
4247 return Err(self.err(alloc::format!(
4248 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4249 )));
4250 }
4251 };
4252 self.advance();
4253 return Ok(Statement::SetConstraints { names, deferred });
4254 }
4255 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4256 // { DEFAULT | '<role>' | <ident> }` (mailrs
4257 // round-10 A.1). pg_dump preamble emits the
4258 // `DEFAULT` form to reset session authorization.
4259 //
4260 // v7.39 (round 697) — this said "SPG has no role system so
4261 // this is a strict no-op". SPG has had one since round 58;
4262 // the comment outlived it, and with it the reason a name
4263 // that is not a role was accepted here. It still switches
4264 // no authorization — what it does now is refuse a role
4265 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4266 // AUTHORIZATION` (handled by the RESET parser
4267 // elsewhere). Reference:
4268 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4269 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4270 {
4271 self.advance(); // AUTHORIZATION
4272 match self.peek().clone() {
4273 Token::Default => {
4274 self.advance();
4275 }
4276 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4277 self.advance();
4278 return Ok(Statement::ValidateOnly {
4279 kind: crate::ast::ValidateOnlyKind::RoleName,
4280 names: alloc::vec![r],
4281 });
4282 }
4283 other => {
4284 return Err(self.err(alloc::format!(
4285 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4286 )));
4287 }
4288 }
4289 return Ok(Statement::Empty);
4290 }
4291 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4292 // ISOLATION LEVEL { READ COMMITTED | READ
4293 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4294 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4295 // PG-standard surface. v7.37.8 accepts the syntax
4296 // and tracks the selected level on
4297 // `Engine::current_isolation_level()`; the actual
4298 // MVCC / SSI semantics implementation lands in
4299 // the 轴 4 isolation framework (separate train).
4300 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4301 // internally; SPG behaves the same (effectively
4302 // READ COMMITTED at every level today).
4303 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4304 {
4305 self.advance(); // TRANSACTION
4306 let modes = self.parse_isolation_level_clauses()?;
4307 return Ok(Statement::SetTransaction { modes });
4308 }
4309 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4310 // alias — same accept-as-no-op as SET NAMES.
4311 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4312 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4313 {
4314 self.advance(); // CHARACTER
4315 self.advance(); // SET
4316 if matches!(
4317 self.peek(),
4318 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4319 ) {
4320 self.advance();
4321 }
4322 return Ok(Statement::Empty);
4323 }
4324 // v7.39 (GUC) — PG spells the timezone GUC as two
4325 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4326 // where <value> is a string/ident or the LOCAL /
4327 // DEFAULT keyword (both mean "back to the default").
4328 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4329 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4330 {
4331 self.advance(); // TIME
4332 self.advance(); // ZONE
4333 let value = match self.peek().clone() {
4334 Token::Ident(s)
4335 if s.eq_ignore_ascii_case("local")
4336 || s.eq_ignore_ascii_case("default") =>
4337 {
4338 self.advance();
4339 crate::ast::SetValue::Default
4340 }
4341 Token::Default => {
4342 self.advance();
4343 crate::ast::SetValue::Default
4344 }
4345 _ => self.parse_set_value()?,
4346 };
4347 return Ok(Statement::SetParameter {
4348 name: "timezone".into(),
4349 value,
4350 local: set_local,
4351 });
4352 }
4353 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4354 // MySQL USER-variable assignment: its own per-session
4355 // namespace, an arbitrary expression on the right, and `:=`
4356 // as a second spelling of `=`. It used to fall into the
4357 // session-PARAMETER list below, whose values are literals and
4358 // whose store nothing reads back under a `@` name — so the
4359 // assignment reported success and vanished.
4360 //
4361 // A `@@`-prefixed LHS is a real engine setting and keeps the
4362 // old path.
4363 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4364 return self.parse_set_user_vars();
4365 }
4366 // v7.14.0 — multi-assignment form
4367 // `SET a = 1, b = 2, …`. Single-assignment is the
4368 // 1-element case. Each LHS may be a regular ident
4369 // or a SessionVar (`@VAR` / `@@VAR`).
4370 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4371 loop {
4372 let lhs = match self.peek().clone() {
4373 Token::SessionVar(s) => {
4374 self.advance();
4375 s
4376 }
4377 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4378 other => {
4379 return Err(self.err(format!(
4380 "expected parameter name after SET, got {other:?}"
4381 )));
4382 }
4383 };
4384 // Accept either `=` or the bare `TO` keyword.
4385 match self.peek() {
4386 Token::Eq => {
4387 self.advance();
4388 }
4389 Token::To => {
4390 self.advance();
4391 }
4392 other => {
4393 return Err(self.err(format!(
4394 "expected `=` or TO after SET {lhs}, got {other:?}"
4395 )));
4396 }
4397 }
4398 let mut value = self.parse_set_value()?;
4399 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4400 // `, name TO` continues a MySQL-style multi-assign,
4401 // anything else is a PG list VALUE
4402 // (`SET search_path = myschema, public`) folded into
4403 // one comma-joined string.
4404 while matches!(self.peek(), Token::Comma) {
4405 let is_assign = matches!(
4406 self.tokens.get(self.pos + 1),
4407 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4408 ) && matches!(
4409 self.tokens.get(self.pos + 2),
4410 Some(Token::Eq | Token::To)
4411 );
4412 if is_assign {
4413 break;
4414 }
4415 self.advance(); // comma
4416 let next = self.parse_set_value()?;
4417 let joined = alloc::format!(
4418 "{}, {}",
4419 set_value_text(&value),
4420 set_value_text(&next)
4421 );
4422 value = crate::ast::SetValue::String(joined);
4423 }
4424 pairs.push((lhs, value));
4425 if matches!(self.peek(), Token::Comma) {
4426 self.advance();
4427 continue;
4428 }
4429 break;
4430 }
4431 if pairs.len() == 1 {
4432 let (name, value) = pairs.into_iter().next().unwrap();
4433 Ok(Statement::SetParameter {
4434 name,
4435 value,
4436 local: set_local,
4437 })
4438 } else {
4439 Ok(Statement::SetParameterList(pairs))
4440 }
4441 }
4442 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4443 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4444 self.advance();
4445 match self.peek().clone() {
4446 Token::All => {
4447 self.advance();
4448 Ok(Statement::ResetParameter(None))
4449 }
4450 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4451 self.advance();
4452 Ok(Statement::ResetParameter(None))
4453 }
4454 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4455 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4456 self.advance();
4457 Ok(Statement::SetRole(None))
4458 }
4459 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4460 // (pg_dump's return from the owner switch).
4461 Token::Ident(s) | Token::QuotedIdent(s)
4462 if s.eq_ignore_ascii_case("session")
4463 && matches!(
4464 self.tokens.get(self.pos + 1),
4465 Some(Token::Ident(a) | Token::QuotedIdent(a))
4466 if a.eq_ignore_ascii_case("authorization")
4467 ) =>
4468 {
4469 self.advance(); // SESSION
4470 self.advance(); // AUTHORIZATION
4471 Ok(Statement::SetRole(None))
4472 }
4473 _ => {
4474 let name = self.parse_set_param_name()?;
4475 Ok(Statement::ResetParameter(Some(name)))
4476 }
4477 }
4478 }
4479 // v7.39 (round 218) — server-side cursors.
4480 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4481 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4482 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4483 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4484 self.advance();
4485 match self.peek().clone() {
4486 Token::All => {
4487 self.advance();
4488 Ok(Statement::CloseCursor { name: None })
4489 }
4490 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4491 self.advance();
4492 Ok(Statement::CloseCursor { name: None })
4493 }
4494 Token::Ident(n) | Token::QuotedIdent(n) => {
4495 self.advance();
4496 Ok(Statement::CloseCursor { name: Some(n) })
4497 }
4498 other => Err(self.err(format!(
4499 "expected cursor name or ALL after CLOSE, got {other:?}"
4500 ))),
4501 }
4502 }
4503 other => Err(self.err(format!(
4504 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4505 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4506 ))),
4507 }
4508 }
4509
4510 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4511 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4512 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4513 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4514 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4515 self.advance(); // DECLARE
4516 let name = match self.advance() {
4517 Token::Ident(n) | Token::QuotedIdent(n) => n,
4518 other => {
4519 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4520 }
4521 };
4522 let mut scroll: Option<bool> = None;
4523 loop {
4524 match self.peek() {
4525 Token::Ident(s)
4526 if s.eq_ignore_ascii_case("binary")
4527 || s.eq_ignore_ascii_case("insensitive")
4528 || s.eq_ignore_ascii_case("asensitive") =>
4529 {
4530 self.advance();
4531 }
4532 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4533 self.advance();
4534 scroll = Some(true);
4535 }
4536 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4537 {
4538 self.advance(); // NO
4539 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4540 return Err(self.err(format!(
4541 "expected SCROLL after NO in DECLARE, got {:?}",
4542 self.peek()
4543 )));
4544 }
4545 self.advance();
4546 scroll = Some(false);
4547 }
4548 _ => break,
4549 }
4550 }
4551 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4552 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4553 }
4554 self.advance();
4555 let mut hold = false;
4556 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4557 self.advance();
4558 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4559 return Err(self.err(format!(
4560 "expected HOLD after WITH in DECLARE, got {:?}",
4561 self.peek()
4562 )));
4563 }
4564 self.advance();
4565 hold = true;
4566 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4567 self.advance();
4568 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4569 return Err(self.err(format!(
4570 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4571 self.peek()
4572 )));
4573 }
4574 self.advance();
4575 }
4576 if !matches!(self.peek(), Token::For) {
4577 return Err(self.err(format!(
4578 "expected FOR before the cursor query, got {:?}",
4579 self.peek()
4580 )));
4581 }
4582 self.advance();
4583 let query = self.parse_one_statement()?;
4584 Ok(Statement::DeclareCursor {
4585 name,
4586 scroll,
4587 hold,
4588 query: alloc::boxed::Box::new(query),
4589 })
4590 }
4591
4592 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4593 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4594 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4595 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4596 use crate::ast::CursorDirection as D;
4597 self.advance(); // FETCH / MOVE
4598 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4599 let neg = if matches!(this.peek(), Token::Minus) {
4600 this.advance();
4601 true
4602 } else {
4603 false
4604 };
4605 match this.advance() {
4606 Token::Integer(v) => Ok(if neg { -v } else { v }),
4607 other => Err(this.err(format!("expected count, got {other:?}"))),
4608 }
4609 };
4610 let direction = match self.peek().clone() {
4611 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4612 self.advance();
4613 D::Next
4614 }
4615 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4616 self.advance();
4617 D::Prior
4618 }
4619 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4620 self.advance();
4621 D::First
4622 }
4623 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4624 self.advance();
4625 D::Last
4626 }
4627 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4628 self.advance();
4629 D::Absolute(signed_count(self)?)
4630 }
4631 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4632 self.advance();
4633 D::Relative(signed_count(self)?)
4634 }
4635 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4636 self.advance();
4637 match self.peek().clone() {
4638 Token::All => {
4639 self.advance();
4640 D::All
4641 }
4642 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4643 self.advance();
4644 D::All
4645 }
4646 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4647 _ => D::Next, // bare FORWARD = FORWARD 1
4648 }
4649 }
4650 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4651 self.advance();
4652 match self.peek().clone() {
4653 Token::All => {
4654 self.advance();
4655 D::BackwardAll
4656 }
4657 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4658 self.advance();
4659 D::BackwardAll
4660 }
4661 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4662 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4663 }
4664 }
4665 Token::All => {
4666 self.advance();
4667 D::All
4668 }
4669 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4670 self.advance();
4671 D::All
4672 }
4673 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4674 // Bare `FETCH <name>` — direction defaults to NEXT.
4675 _ => D::Next,
4676 };
4677 // Optional FROM / IN.
4678 if matches!(self.peek(), Token::From)
4679 || matches!(self.peek(), Token::In)
4680 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4681 {
4682 self.advance();
4683 }
4684 let name = match self.advance() {
4685 Token::Ident(n) | Token::QuotedIdent(n) => n,
4686 other => {
4687 return Err(self.err(format!("expected cursor name, got {other:?}")));
4688 }
4689 };
4690 Ok(if is_move {
4691 Statement::MoveCursor { name, direction }
4692 } else {
4693 Statement::FetchCursor { name, direction }
4694 })
4695 }
4696
4697 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4698 /// [(kind, …)] ON <col>, … FROM <table>`.
4699 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4700 self.advance(); // STATISTICS
4701 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4702 let mut if_not_exists = false;
4703 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4704 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4705 {
4706 self.advance();
4707 self.advance();
4708 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4709 self.advance();
4710 if_not_exists = true;
4711 }
4712 }
4713 let name = self.expect_ident_like()?;
4714 let mut kinds = Vec::new();
4715 if matches!(self.peek(), Token::LParen) {
4716 self.advance();
4717 loop {
4718 let k = self.expect_ident_like()?;
4719 // PG stores the single letters; accept the spelled-out
4720 // names the SQL uses and record what PG records.
4721 kinds.push(match k.to_ascii_lowercase().as_str() {
4722 "ndistinct" => String::from("d"),
4723 "dependencies" => String::from("f"),
4724 "mcv" => String::from("m"),
4725 other => {
4726 return Err(
4727 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4728 );
4729 }
4730 });
4731 match self.advance() {
4732 Token::Comma => {}
4733 Token::RParen => break,
4734 other => {
4735 return Err(self.err(alloc::format!(
4736 "expected ',' or ')' in statistics kind list, got {other:?}"
4737 )));
4738 }
4739 }
4740 }
4741 }
4742 if !matches!(self.peek(), Token::On) {
4743 return Err(self.err(alloc::format!(
4744 "expected ON in CREATE STATISTICS, got {:?}",
4745 self.peek()
4746 )));
4747 }
4748 self.advance();
4749 let mut columns = Vec::new();
4750 loop {
4751 columns.push(self.expect_ident_like()?);
4752 if matches!(self.peek(), Token::Comma) {
4753 self.advance();
4754 } else {
4755 break;
4756 }
4757 }
4758 if !matches!(self.peek(), Token::From) {
4759 return Err(self.err(alloc::format!(
4760 "expected FROM in CREATE STATISTICS, got {:?}",
4761 self.peek()
4762 )));
4763 }
4764 self.advance();
4765 let table = self.expect_ident_like()?;
4766 Ok(Statement::CreateStatistics {
4767 name,
4768 if_not_exists,
4769 kinds,
4770 columns,
4771 table,
4772 })
4773 }
4774
4775 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4776 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4777 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4778 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4779 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4780 /// forward call.
4781 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4782 self.advance(); // TABLE
4783 let if_exists = self.consume_if_exists();
4784 let mut names: Vec<String> = Vec::new();
4785 loop {
4786 names.push(self.expect_ident_like()?);
4787 if matches!(self.peek(), Token::Comma) {
4788 self.advance();
4789 continue;
4790 }
4791 break;
4792 }
4793 if matches!(
4794 self.peek(),
4795 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4796 || s.eq_ignore_ascii_case("restrict")
4797 ) {
4798 self.advance();
4799 }
4800 Ok(Statement::DropTable { names, if_exists })
4801 }
4802
4803 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4804 self.advance(); // STATISTICS
4805 let mut if_exists = false;
4806 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4807 && matches!(self.tokens.get(self.pos + 1),
4808 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4809 {
4810 self.advance();
4811 self.advance();
4812 if_exists = true;
4813 }
4814 let name = self.expect_ident_like()?;
4815 Ok(Statement::DropStatistics { name, if_exists })
4816 }
4817
4818 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4819 debug_assert!(matches!(self.peek(), Token::Create));
4820 self.advance();
4821 match self.peek() {
4822 Token::Table => self.parse_create_table_stmt_after_create(),
4823 Token::Index => self.parse_create_index_stmt_after_create(false),
4824 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4825 // object now. It used to be consumed by the CREATE-noise
4826 // arm, so a pg_dump that declares extended statistics
4827 // restored silently without them and reflection showed
4828 // nothing.
4829 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4830 self.parse_create_statistics_after_create()
4831 }
4832 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4833 // The `UNIQUE` modifier turns a partial index into a
4834 // partial-uniqueness invariant (only rows matching the
4835 // WHERE predicate are checked for duplicates). mailrs
4836 // K1 (3 hits: email_templates default, calendar_events
4837 // master, calendar_events instance).
4838 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4839 self.advance();
4840 if !matches!(self.peek(), Token::Index) {
4841 return Err(self.err(alloc::format!(
4842 "expected INDEX after CREATE UNIQUE, got {:?}",
4843 self.peek()
4844 )));
4845 }
4846 self.parse_create_index_stmt_after_create(true)
4847 }
4848 Token::Publication => {
4849 self.advance();
4850 self.parse_create_publication_after_keyword()
4851 }
4852 Token::Subscription => {
4853 self.advance();
4854 self.parse_create_subscription_after_keyword()
4855 }
4856 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4857 // USER isn't a reserved keyword — we look for the bare
4858 // identifier so the lexer doesn't have to grow a token.
4859 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4860 self.advance();
4861 self.parse_create_user_after_keyword(true)
4862 }
4863 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4864 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4865 // the default of the LOGIN attribute.
4866 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4867 self.advance();
4868 self.parse_create_user_after_keyword(false)
4869 }
4870 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4871 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4872 self.advance();
4873 self.parse_create_policy_after_keyword()
4874 }
4875 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4876 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4877 // no-op. mailrs follow-up F3.
4878 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4879 self.advance();
4880 self.parse_create_extension_after_keyword()
4881 }
4882 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4883 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4884 // optional; absorb it here and forward to the
4885 // per-kind parsers with the flag. OR is a reserved
4886 // keyword token.
4887 Token::Or => {
4888 self.advance();
4889 let next = self.peek();
4890 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4891 return Err(self.err(alloc::format!(
4892 "expected REPLACE after CREATE OR, got {next:?}"
4893 )));
4894 };
4895 if !s2.eq_ignore_ascii_case("replace") {
4896 return Err(self.err(alloc::format!(
4897 "expected REPLACE after CREATE OR, got {s2:?}"
4898 )));
4899 }
4900 self.advance();
4901 self.parse_create_function_or_trigger_after_or_replace(true)
4902 }
4903 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4904 self.advance();
4905 self.parse_create_function_after_keyword(false)
4906 }
4907 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4908 self.advance();
4909 self.parse_create_trigger_after_keyword(false)
4910 }
4911 // v7.39 (round 139) — CREATE RULE …
4912 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4913 self.advance();
4914 self.parse_create_rule_after_keyword(false)
4915 }
4916 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4917 // trigger is a row-level AFTER trigger that additionally carries
4918 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4919 // path already tolerates and skips those clauses, so consuming the
4920 // CONSTRAINT keyword and reusing it makes the statement parse and the
4921 // trigger fire. (The deferral timing itself is not yet honoured —
4922 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4923 // for every non-deferred use.)
4924 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4925 self.advance();
4926 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4927 if t.eq_ignore_ascii_case("trigger"))
4928 {
4929 return Err(self.err(alloc::format!(
4930 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4931 self.peek()
4932 )));
4933 }
4934 self.advance();
4935 self.parse_create_trigger_after_keyword(false)
4936 }
4937 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4938 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4939 self.advance();
4940 self.parse_create_sequence_after_keyword(false)
4941 }
4942 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4943 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4944 self.advance();
4945 self.parse_create_view_after_keyword(false, false, false)
4946 }
4947 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4948 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4949 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4950 // appear (in any order) between `CREATE` and `VIEW` in
4951 // every mysqldump-emitted view. Pre-2.6 the parser
4952 // rejected the prefix and the customer's whole view
4953 // backup failed on the first view. The hints are pure
4954 // planner / permission metadata; SPG's view-rewrite
4955 // path is semantically equivalent for all three
4956 // algorithms in v7.17 (TEMPTABLE differs only in
4957 // perf for huge views — out of v7.17 scope), and
4958 // DEFINER / SQL SECURITY are pure single-user
4959 // permissioning that SPG ignores by design.
4960 Token::Ident(s) | Token::QuotedIdent(s)
4961 if s.eq_ignore_ascii_case("algorithm")
4962 || s.eq_ignore_ascii_case("definer")
4963 || s.eq_ignore_ascii_case("sql") =>
4964 {
4965 self.consume_mysql_view_prefix()?;
4966 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4967 // (in any order, in any combination), the next
4968 // keyword must be VIEW. mysqldump never emits these
4969 // prefixes on non-view statements.
4970 let next = self.peek().clone();
4971 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4972 if s2.eq_ignore_ascii_case("view"))
4973 {
4974 self.advance();
4975 self.parse_create_view_after_keyword(false, false, false)
4976 } else {
4977 Err(self.err(alloc::format!(
4978 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4979 )))
4980 }
4981 }
4982 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4983 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4984 self.advance();
4985 self.parse_create_type_after_keyword()
4986 }
4987 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4988 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4989 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4990 self.advance();
4991 self.parse_create_domain_after_keyword()
4992 }
4993 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4994 // name [AUTHORIZATION user]. Real catalog registry
4995 // (was silent-no-op'd pre-v7.17).
4996 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4997 self.advance();
4998 let if_not_exists = self.parse_if_not_exists();
4999 let name = self.expect_ident_like()?;
5000 // Optional `AUTHORIZATION <user>` trailer — accepted,
5001 // ignored (single-user catalog).
5002 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5003 if s.eq_ignore_ascii_case("authorization"))
5004 {
5005 self.advance();
5006 let _ = self.expect_ident_like()?;
5007 }
5008 Ok(Statement::CreateSchema { name, if_not_exists })
5009 }
5010 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5011 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5012 self.advance();
5013 let next = self.peek().clone();
5014 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5015 {
5016 self.advance();
5017 self.parse_create_materialized_view_after_keyword()
5018 } else {
5019 Err(self.err(alloc::format!(
5020 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5021 )))
5022 }
5023 }
5024 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5025 // no-op below), an UNLOGGED table is a real, fully-usable table in
5026 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5027 // durability optimisation is a follow-up), so a dump / app that
5028 // declares UNLOGGED tables works instead of failing to parse.
5029 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5030 self.advance(); // UNLOGGED
5031 if matches!(self.peek(), Token::Table) {
5032 self.parse_create_table_stmt_after_create()
5033 } else {
5034 Err(self.err(format!(
5035 "expected TABLE after CREATE UNLOGGED, got {:?}",
5036 self.peek()
5037 )))
5038 }
5039 }
5040 Token::Ident(s) | Token::QuotedIdent(s)
5041 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5042 {
5043 self.advance();
5044 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5045 let next = self.peek().clone();
5046 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5047 {
5048 self.advance();
5049 self.parse_create_sequence_after_keyword(true)
5050 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5051 {
5052 self.advance();
5053 self.parse_create_view_after_keyword(false, false, true)
5054 } else {
5055 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5056 // consumed and answered OK while creating nothing, so
5057 // every statement that touched the table afterwards failed
5058 // with "table not found" — the DDL itself lied. It is a
5059 // real CREATE TABLE now, marked temporary so the executor
5060 // puts it in the session's own namespace. An optional
5061 // TABLE keyword may or may not be present (`CREATE TEMP t`
5062 // is not legal, but the keyword is consumed by the
5063 // CREATE TABLE parser itself).
5064 let stmt = self.parse_create_table_stmt_after_create()?;
5065 match stmt {
5066 Statement::CreateTable(mut c) => {
5067 c.temporary = true;
5068 Ok(Statement::CreateTable(c))
5069 }
5070 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5071 // CTAS node, which needs the same session namespace.
5072 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5073 m.temporary = true;
5074 Ok(Statement::CreateMaterializedView(m))
5075 }
5076 other => Ok(other),
5077 }
5078 }
5079 }
5080 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5081 // BEGIN <body> END`. The body may reference `@var`
5082 // session variables, SET statements, internal `;`
5083 // terminators, etc. SPG has no procedure runtime, so
5084 // consume the whole `CREATE PROCEDURE … END` block as
5085 // a no-op so mysqldump scripts that include stored
5086 // routines load through. The matching-END consumer
5087 // tracks BEGIN/END nesting depth to handle nested
5088 // BEGIN blocks correctly.
5089 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5090 self.consume_mysql_routine_body();
5091 Ok(Statement::Empty)
5092 }
5093 // v7.14.0 — pg_dump / mysqldump emit
5094 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5095 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5096 // SPG is single-schema / single-database; these have
5097 // no behavioural effect, so consume + return Empty.
5098 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5099 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5100 // moved up to real parser branches. DATABASE / ROLE /
5101 // POLICY / OPERATOR stay no-op forever
5102 // (single-database, hardcoded roles).
5103 Token::Ident(s) | Token::QuotedIdent(s)
5104 if matches!(
5105 s.to_ascii_lowercase().as_str(),
5106 "database"
5107 | "role"
5108 | "operator"
5109 | "cast"
5110 | "aggregate"
5111 | "language"
5112 | "collation"
5113 | "conversion"
5114 // v7.17.0 Phase 8 (audit N6) — rarely-
5115 // emitted pg_dump shapes that should
5116 // load through without a parser error.
5117 // SPG has no planner statistics catalog,
5118 // no event-trigger hooks, no foreign-
5119 // data-wrapper infrastructure; consume
5120 // + return Empty.
5121 | "statistics"
5122 | "event"
5123 // v7.37.17 (17.6 siblings) — additional CREATE
5124 // targets pg_dump / operator install scripts
5125 // may emit that SPG has no matching machinery
5126 // for. Consume + Empty-return.
5127 | "text"
5128 | "tablespace"
5129 | "access"
5130 | "large"
5131 ) =>
5132 {
5133 // DATABASE is the one member of this list PG refuses
5134 // inside a transaction block; the rest (ROLE, CAST,
5135 // TABLESPACE, …) it runs there quite happily, so only
5136 // this one is named. Still a no-op otherwise — SPG is
5137 // single-database.
5138 let is_database = s.eq_ignore_ascii_case("database");
5139 // The name is the first token after DATABASE, past an
5140 // `IF NOT EXISTS`.
5141 let name = if is_database {
5142 self.scan_database_name()
5143 } else {
5144 None
5145 };
5146 let collation = if is_database {
5147 self.scan_database_collation_until_boundary()
5148 } else {
5149 self.consume_until_statement_boundary();
5150 None
5151 };
5152 if is_database {
5153 return Ok(Statement::NoOpPreventedInTransaction {
5154 what: String::from("CREATE DATABASE"),
5155 collation,
5156 name,
5157 });
5158 }
5159 Ok(Statement::Empty)
5160 }
5161 // v7.39 (round 706) — the foreign-data family leaves the silent
5162 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5163 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5164 // FDW machinery), but the ENGINE now warns, so a restore log
5165 // says what will not function instead of reporting success.
5166 Token::Ident(s) | Token::QuotedIdent(s)
5167 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5168 {
5169 self.consume_until_statement_boundary();
5170 Ok(Statement::ValidateOnly {
5171 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5172 names: Vec::new(),
5173 })
5174 }
5175 other => Err(self.err(format!(
5176 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5177 ))),
5178 }
5179 }
5180
5181 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5182 /// keyword decides whether we parse a function or trigger
5183 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5184 /// PROCEDURE) — those land in later releases.
5185 fn parse_create_function_or_trigger_after_or_replace(
5186 &mut self,
5187 or_replace: bool,
5188 ) -> Result<Statement, ParseError> {
5189 let tok = self.peek();
5190 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5191 return Err(self.err(alloc::format!(
5192 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5193 )));
5194 };
5195 if s.eq_ignore_ascii_case("function") {
5196 self.advance();
5197 self.parse_create_function_after_keyword(or_replace)
5198 } else if s.eq_ignore_ascii_case("trigger") {
5199 self.advance();
5200 self.parse_create_trigger_after_keyword(or_replace)
5201 } else if s.eq_ignore_ascii_case("rule") {
5202 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5203 self.advance();
5204 self.parse_create_rule_after_keyword(or_replace)
5205 } else if s.eq_ignore_ascii_case("view") {
5206 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5207 self.advance();
5208 self.parse_create_view_after_keyword(or_replace, false, false)
5209 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5210 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5211 self.advance();
5212 let nxt = self.peek().clone();
5213 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5214 {
5215 self.advance();
5216 self.parse_create_view_after_keyword(or_replace, false, true)
5217 } else {
5218 Err(self.err(alloc::format!(
5219 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5220 )))
5221 }
5222 } else {
5223 Err(self.err(alloc::format!(
5224 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5225 )))
5226 }
5227 }
5228
5229 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5230 /// SPG doesn't have a registry; pgvector / similar are
5231 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5232 /// the syntax lets dual-target schemas keep the line.
5233 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5234 // Optional `IF NOT EXISTS`.
5235 self.consume_if_not_exists();
5236 let name = self.expect_ident_like()?;
5237 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5238 // CASCADE / FROM '<v>' clauses; we don't model them.
5239 loop {
5240 match self.peek() {
5241 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5242 self.advance();
5243 continue;
5244 }
5245 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5246 self.advance();
5247 let _ = self.expect_ident_like()?;
5248 continue;
5249 }
5250 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5251 self.advance();
5252 // String or ident literal.
5253 let _ = self.advance();
5254 continue;
5255 }
5256 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5257 self.advance();
5258 let _ = self.advance();
5259 continue;
5260 }
5261 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5262 self.advance();
5263 continue;
5264 }
5265 _ => break,
5266 }
5267 }
5268 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5269 // nosuch` reported success and `pg_extension` then did not list it,
5270 // which is the accept-and-do-nothing shape F31 exists to find.
5271 Ok(Statement::ValidateOnly {
5272 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5273 names: alloc::vec![name],
5274 })
5275 }
5276
5277 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5278 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5279 /// already been consumed by the caller. Grammar accepted:
5280 ///
5281 /// name `(` arg-list `)`
5282 /// `RETURNS` return-type
5283 /// [ `LANGUAGE` ident ]
5284 /// `AS` $$ body $$
5285 /// [ `LANGUAGE` ident ]
5286 ///
5287 /// Either `LANGUAGE` position is allowed; PG accepts both.
5288 fn parse_create_function_after_keyword(
5289 &mut self,
5290 or_replace: bool,
5291 ) -> Result<Statement, ParseError> {
5292 let name = self.expect_ident_like()?;
5293 // Argument list. v7.12.4 commonly sees the empty `()`
5294 // (trigger functions); typed args parse and round-trip
5295 // but the executor only invokes nullary functions.
5296 if !matches!(self.peek(), Token::LParen) {
5297 return Err(self.err(alloc::format!(
5298 "expected '(' after function name {name:?}, got {:?}",
5299 self.peek()
5300 )));
5301 }
5302 self.advance();
5303 let args = self.parse_function_arg_list()?;
5304 // RETURNS clause.
5305 let tok = self.peek();
5306 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5307 return Err(self.err(alloc::format!(
5308 "expected RETURNS after function arg list, got {tok:?}"
5309 )));
5310 };
5311 if !s.eq_ignore_ascii_case("returns") {
5312 return Err(self.err(alloc::format!(
5313 "expected RETURNS after function arg list, got {s:?}"
5314 )));
5315 }
5316 self.advance();
5317 let returns = self.parse_function_return()?;
5318 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5319 // re-check after the body too).
5320 let mut language: Option<String> = self.parse_optional_language()?;
5321 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5322 // either side of the body and in any order, interleaved with
5323 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5324 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5325 // PG's own pg_dump output did not restore.
5326 let mut attrs = FunctionAttrs::default();
5327 loop {
5328 let before = self.pos;
5329 self.parse_function_attrs_into(&mut attrs)?;
5330 if language.is_none() {
5331 language = self.parse_optional_language()?;
5332 }
5333 if self.pos == before {
5334 break;
5335 }
5336 }
5337 // `AS` followed by a $$-quoted body (lexer already
5338 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5339 // Token::String). AS is a reserved keyword (Token::As).
5340 if !matches!(self.peek(), Token::As) {
5341 return Err(self.err(alloc::format!(
5342 "expected AS before function body, got {:?}",
5343 self.peek()
5344 )));
5345 }
5346 self.advance();
5347 let body_text = match self.peek() {
5348 Token::String(s) => {
5349 let body = s.clone();
5350 self.advance();
5351 body
5352 }
5353 other => {
5354 return Err(self.err(alloc::format!(
5355 "expected $$-quoted function body after AS, got {other:?}"
5356 )));
5357 }
5358 };
5359 // Trailing clauses — PG's other accepted position for both the
5360 // LANGUAGE and the attributes.
5361 loop {
5362 let before = self.pos;
5363 self.parse_function_attrs_into(&mut attrs)?;
5364 if language.is_none() {
5365 language = self.parse_optional_language()?;
5366 }
5367 if self.pos == before {
5368 break;
5369 }
5370 }
5371 let language = language.unwrap_or_else(|| String::from("sql"));
5372 // PL/pgSQL bodies get structure-parsed. Other languages
5373 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5374 // recognise) round-trip as Raw text — the executor errors
5375 // when invoked with a clear unsupported message.
5376 let body = if language.eq_ignore_ascii_case("plpgsql") {
5377 match parse_plpgsql_body(&body_text) {
5378 Ok(block) => FunctionBody::PlPgSql(block),
5379 // Best-effort: if the body parser doesn't yet
5380 // support a construct used inside, fall back to
5381 // raw — keeps `CREATE FUNCTION` itself working
5382 // (catalogue accepts), executor errors on
5383 // invocation only.
5384 Err(_) => FunctionBody::Raw(body_text),
5385 }
5386 } else {
5387 FunctionBody::Raw(body_text)
5388 };
5389 Ok(Statement::CreateFunction(CreateFunctionStatement {
5390 name,
5391 or_replace,
5392 args,
5393 returns,
5394 language,
5395 body,
5396 attrs,
5397 }))
5398 }
5399
5400 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5401 /// attribute clauses into `attrs`, stopping at the first token that
5402 /// is not one. Measured against PG 18.4, which accepts them in any
5403 /// order and on either side of the body.
5404 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5405 loop {
5406 let word = match self.peek() {
5407 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5408 // NOT LEAKPROOF — NOT is a reserved keyword token.
5409 Token::Not
5410 if matches!(
5411 self.tokens.get(self.pos + 1),
5412 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5413 ) =>
5414 {
5415 self.advance();
5416 self.advance();
5417 attrs.leakproof = false;
5418 continue;
5419 }
5420 _ => return Ok(()),
5421 };
5422 match word.as_str() {
5423 "immutable" => {
5424 self.advance();
5425 attrs.volatility = FunctionVolatility::Immutable;
5426 }
5427 "stable" => {
5428 self.advance();
5429 attrs.volatility = FunctionVolatility::Stable;
5430 }
5431 "volatile" => {
5432 self.advance();
5433 attrs.volatility = FunctionVolatility::Volatile;
5434 }
5435 "strict" => {
5436 self.advance();
5437 attrs.strict = true;
5438 }
5439 "leakproof" => {
5440 self.advance();
5441 attrs.leakproof = true;
5442 }
5443 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5444 // spelled-out forms of STRICT and its opposite.
5445 "returns" | "called" => {
5446 let strict = word == "returns";
5447 let mut probe = self.pos + 1;
5448 if strict {
5449 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5450 // is not ours.
5451 match self.tokens.get(probe) {
5452 Some(Token::Null) => probe += 1,
5453 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5454 _ => return Ok(()),
5455 }
5456 }
5457 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5458 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5459 if !ok {
5460 return Ok(());
5461 }
5462 probe += 1;
5463 match self.tokens.get(probe) {
5464 Some(Token::Null) => probe += 1,
5465 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5466 _ => return Ok(()),
5467 }
5468 match self.tokens.get(probe) {
5469 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5470 _ => return Ok(()),
5471 }
5472 self.pos = probe;
5473 attrs.strict = strict;
5474 }
5475 "security" | "external" => {
5476 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5477 let mut probe = self.pos + 1;
5478 if word == "external" {
5479 match self.tokens.get(probe) {
5480 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5481 probe += 1;
5482 }
5483 _ => return Ok(()),
5484 }
5485 }
5486 let definer = match self.tokens.get(probe) {
5487 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5488 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5489 _ => return Ok(()),
5490 };
5491 self.pos = probe + 1;
5492 attrs.security_definer = definer;
5493 }
5494 "parallel" => {
5495 let level = match self.tokens.get(self.pos + 1) {
5496 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5497 FunctionParallel::Safe
5498 }
5499 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5500 FunctionParallel::Restricted
5501 }
5502 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5503 FunctionParallel::Unsafe
5504 }
5505 _ => return Ok(()),
5506 };
5507 self.pos += 2;
5508 attrs.parallel = level;
5509 }
5510 "cost" | "rows" => {
5511 let Some(n) = self.peek_number_at(self.pos + 1) else {
5512 return Ok(());
5513 };
5514 self.pos += 2;
5515 if word == "cost" {
5516 attrs.cost = Some(n);
5517 } else {
5518 attrs.rows = Some(n);
5519 }
5520 }
5521 _ => return Ok(()),
5522 }
5523 }
5524 }
5525
5526 /// The numeric literal at `idx`, if there is one.
5527 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5528 match self.tokens.get(idx)? {
5529 Token::Integer(n) => Some(*n as f64),
5530 Token::Float(f) => Some(*f),
5531 Token::Numeric(t) => t.parse::<f64>().ok(),
5532 _ => None,
5533 }
5534 }
5535
5536 /// Closing `)`-terminated argument list. v7.12.4 commonly
5537 /// sees the empty `()`; typed args round-trip but the
5538 /// executor (yet) doesn't invoke them.
5539 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5540 /// it away, which is what PG does with one on a function parameter.
5541 fn skip_type_modifier(&mut self) {
5542 if !matches!(self.peek(), Token::LParen) {
5543 return;
5544 }
5545 // Only a numeric modifier — anything else is not one, and eating
5546 // it would swallow real grammar.
5547 let mut i = self.pos + 1;
5548 let mut seen_number = false;
5549 loop {
5550 match self.tokens.get(i) {
5551 Some(Token::Integer(_)) => seen_number = true,
5552 Some(Token::Comma) => {}
5553 Some(Token::RParen) => break,
5554 _ => return,
5555 }
5556 i += 1;
5557 }
5558 if !seen_number {
5559 return;
5560 }
5561 while self.pos <= i {
5562 self.advance();
5563 }
5564 }
5565
5566 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5567 let mut args: Vec<FunctionArg> = Vec::new();
5568 if matches!(self.peek(), Token::RParen) {
5569 self.advance();
5570 return Ok(args);
5571 }
5572 loop {
5573 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5574 // a reserved token; OUT / INOUT are bare idents.
5575 let mode = if matches!(self.peek(), Token::In) {
5576 self.advance();
5577 FunctionArgMode::In
5578 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5579 {
5580 self.advance();
5581 FunctionArgMode::Out
5582 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5583 {
5584 self.advance();
5585 FunctionArgMode::InOut
5586 } else {
5587 FunctionArgMode::In
5588 };
5589 // Optional name. The next token is either a name
5590 // (followed by a type ident) or the type itself.
5591 // Disambiguate by peeking ahead: if the token after
5592 // the next ident is also an ident, we treat the
5593 // first as the name.
5594 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5595 // the comma or paren, then decide. Reading at most two of
5596 // them could not spell `x double precision` at all, and
5597 // silently mis-read the bare `double precision` as a
5598 // parameter named "double" — which is what made the same
5599 // signature key two different ways.
5600 let (name, ty_token) = {
5601 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5602 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5603 words.push(self.expect_ident_like()?);
5604 }
5605 // v7.39 (round 344) — a length / precision modifier on the
5606 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5607 // accepts it and DROPS it — `pg_get_function_arguments`
5608 // reports plain `character varying` / `numeric`, measured on
5609 // 18.4 — but SPG raised `syntax error at or near "("`,
5610 // because the modifier's parens were never consumed.
5611 self.skip_type_modifier();
5612 // r1049 — `f(v bigint[])`. The array suffix parsed in
5613 // the column position, the cast position and (r1038)
5614 // the RETURNS position, but not here: the fifth
5615 // member of the same family, reported by sentori as
5616 // presumably the same code. It is now.
5617 let array_suffix = self.consume_array_suffix();
5618 let whole = words.join(" ");
5619 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5620 {
5621 (Some(words[0].clone()), words[1..].join(" "))
5622 } else {
5623 (None, whole)
5624 };
5625 ty_token.push_str(&array_suffix);
5626 (name, ty_token)
5627 };
5628 // Type — try to map to ColumnTypeName, else Raw.
5629 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5630 Some(t) => FunctionArgType::Typed(t),
5631 None => FunctionArgType::Raw(ty_token),
5632 };
5633 args.push(FunctionArg { mode, name, ty });
5634 match self.peek() {
5635 Token::Comma => {
5636 self.advance();
5637 continue;
5638 }
5639 Token::RParen => {
5640 self.advance();
5641 return Ok(args);
5642 }
5643 other => {
5644 return Err(self.err(alloc::format!(
5645 "expected , or ) in function arg list, got {other:?}"
5646 )));
5647 }
5648 }
5649 }
5650 }
5651
5652 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5653 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5654 // function whose row shape is named inline.
5655 if matches!(self.peek(), Token::Table)
5656 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5657 {
5658 self.advance(); // TABLE
5659 self.advance(); // (
5660 let mut cols: Vec<String> = Vec::new();
5661 loop {
5662 let cname = self.expect_ident_like()?;
5663 let mut ty: Vec<String> = Vec::new();
5664 loop {
5665 match self.peek() {
5666 Token::Comma | Token::RParen | Token::Eof => break,
5667 _ => {}
5668 }
5669 match self.advance() {
5670 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5671 other => {
5672 if let Some(w) = unreserved_keyword_text(&other) {
5673 ty.push(w);
5674 }
5675 }
5676 }
5677 }
5678 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5679 if matches!(self.peek(), Token::Comma) {
5680 self.advance();
5681 } else {
5682 break;
5683 }
5684 }
5685 if matches!(self.peek(), Token::RParen) {
5686 self.advance();
5687 }
5688 return Ok(FunctionReturn::Other(alloc::format!(
5689 "TABLE({})",
5690 cols.join(", ")
5691 )));
5692 }
5693 let ident = self.expect_ident_like()?;
5694 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5695 if ident.eq_ignore_ascii_case("setof") {
5696 let inner = self.expect_ident_like()?;
5697 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5698 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5699 }
5700 if ident.eq_ignore_ascii_case("trigger") {
5701 return Ok(FunctionReturn::Trigger);
5702 }
5703 if ident.eq_ignore_ascii_case("void") {
5704 return Ok(FunctionReturn::Void);
5705 }
5706 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5707 // RETURN position did not, so the `[` was a syntax error and the
5708 // whole migration stopped. sentori worked around it by returning
5709 // zero-padded text.
5710 let suffix = self.consume_array_suffix();
5711 if !suffix.is_empty() {
5712 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5713 }
5714 match map_type_ident_to_column_type_name(&ident) {
5715 Some(t) => Ok(FunctionReturn::Type(t)),
5716 None => Ok(FunctionReturn::Other(ident)),
5717 }
5718 }
5719
5720 /// Consume any `[]` / `[N]` array markers after a type name and give
5721 /// back their text. Empty when there are none.
5722 fn consume_array_suffix(&mut self) -> String {
5723 let mut out = String::new();
5724 while matches!(self.peek(), Token::LBracket) {
5725 self.advance();
5726 // `[N]` is accepted and, as in PG, the length is not enforced.
5727 if let Token::Integer(n) = self.peek().clone() {
5728 self.advance();
5729 out.push_str(&alloc::format!("[{n}]"));
5730 } else {
5731 out.push_str("[]");
5732 }
5733 if matches!(self.peek(), Token::RBracket) {
5734 self.advance();
5735 }
5736 }
5737 out
5738 }
5739
5740 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5741 match self.peek() {
5742 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5743 self.advance();
5744 let lang = self.expect_ident_like()?;
5745 Ok(Some(lang.to_ascii_lowercase()))
5746 }
5747 _ => Ok(None),
5748 }
5749 }
5750
5751 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5752 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5753 /// (expr)]*`. The `DOMAIN` keyword has already been
5754 /// consumed. PG allows the trailing constraints in any
5755 /// order; we approximate with a small loop.
5756 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5757 let name = self.expect_ident_like()?;
5758 // Optional `AS`.
5759 if matches!(self.peek(), Token::As) {
5760 self.advance();
5761 }
5762 // v7.39 (round 259) — keep the raw type NAME when the base is not
5763 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5764 // parent domain.
5765 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5766 self.parse_type_with_implied_flags()?;
5767 let mut default: Option<Expr> = None;
5768 let mut not_null = false;
5769 let mut checks: Vec<Expr> = Vec::new();
5770 loop {
5771 match self.peek() {
5772 Token::Default => {
5773 if default.is_some() {
5774 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5775 }
5776 self.advance();
5777 default = Some(self.parse_expr(0)?);
5778 }
5779 Token::Not => {
5780 self.advance();
5781 if !matches!(self.peek(), Token::Null) {
5782 return Err(self.err(alloc::format!(
5783 "expected NULL after NOT in DOMAIN, got {:?}",
5784 self.peek()
5785 )));
5786 }
5787 self.advance();
5788 not_null = true;
5789 }
5790 Token::Null => {
5791 self.advance();
5792 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5793 // is the default-nullable marker (PG accepts it),
5794 // but AFTER a NOT NULL it is a conflict PG refuses
5795 // (`conflicting NULL/NOT NULL constraints`,
5796 // PG18-measured); the old arm no-opped both ways.
5797 if not_null {
5798 return Err(self.err(alloc::string::String::from(
5799 "conflicting NULL/NOT NULL constraints",
5800 )));
5801 }
5802 }
5803 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5804 self.advance();
5805 if !matches!(self.peek(), Token::LParen) {
5806 return Err(self.err(alloc::format!(
5807 "expected '(' after CHECK in DOMAIN, got {:?}",
5808 self.peek()
5809 )));
5810 }
5811 self.advance();
5812 let expr = self.parse_expr(0)?;
5813 if !matches!(self.peek(), Token::RParen) {
5814 return Err(self.err(alloc::format!(
5815 "expected ')' after CHECK expr, got {:?}",
5816 self.peek()
5817 )));
5818 }
5819 self.advance();
5820 checks.push(expr);
5821 }
5822 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5823 // prefix on the constraint; we drop the name and
5824 // recurse into the constraint parsing.
5825 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5826 self.advance();
5827 let _ = self.expect_ident_like()?;
5828 }
5829 _ => break,
5830 }
5831 }
5832 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5833 name,
5834 base_type,
5835 base_domain: base_user_ref,
5836 default,
5837 not_null,
5838 checks,
5839 }))
5840 }
5841
5842 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5843 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5844 /// consumed.
5845 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5846 let name = self.expect_ident_like()?;
5847 // Required `AS`.
5848 if !matches!(self.peek(), Token::As) {
5849 return Err(self.err(alloc::format!(
5850 "expected AS after CREATE TYPE {name:?}, got {:?}",
5851 self.peek()
5852 )));
5853 }
5854 self.advance();
5855 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5856 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5857 // on the next token: `(` = composite, ident `ENUM` = enum.
5858 if matches!(self.peek(), Token::LParen) {
5859 self.advance();
5860 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5861 let mut field_user_types: Vec<Option<String>> = Vec::new();
5862 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5863 // is legal PG (an attribute-less composite; measured — the old
5864 // e2e note claimed PG requires at least one attribute).
5865 if matches!(self.peek(), Token::RParen) {
5866 self.advance();
5867 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5868 name,
5869 kind: crate::ast::TypeKind::Composite {
5870 fields,
5871 field_user_types,
5872 },
5873 }));
5874 }
5875 loop {
5876 let field_name = self.expect_ident_like()?;
5877 // v7.39 (round 264) — keep the raw type name when it is not
5878 // a builtin: that is how a NESTED composite field records
5879 // which composite it holds.
5880 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5881 self.parse_type_with_implied_flags()?;
5882 fields.push((field_name, field_type));
5883 field_user_types.push(field_user_ref);
5884 if matches!(self.peek(), Token::Comma) {
5885 self.advance();
5886 continue;
5887 }
5888 if matches!(self.peek(), Token::RParen) {
5889 self.advance();
5890 break;
5891 }
5892 return Err(self.err(alloc::format!(
5893 "expected , or ) in composite field list, got {:?}",
5894 self.peek()
5895 )));
5896 }
5897 if fields.is_empty() {
5898 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5899 }
5900 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5901 name,
5902 kind: crate::ast::TypeKind::Composite {
5903 fields,
5904 field_user_types,
5905 },
5906 }));
5907 }
5908 // Required `ENUM` ident.
5909 let kind_ident = match self.peek().clone() {
5910 Token::Ident(s) | Token::QuotedIdent(s) => s,
5911 other => {
5912 return Err(self.err(alloc::format!(
5913 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5914 )));
5915 }
5916 };
5917 if !kind_ident.eq_ignore_ascii_case("enum") {
5918 return Err(self.err(alloc::format!(
5919 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5920 )));
5921 }
5922 self.advance();
5923 if !matches!(self.peek(), Token::LParen) {
5924 return Err(self.err(alloc::format!(
5925 "expected '(' after ENUM, got {:?}",
5926 self.peek()
5927 )));
5928 }
5929 self.advance();
5930 let mut labels: Vec<String> = Vec::new();
5931 loop {
5932 match self.peek().clone() {
5933 Token::String(s) => {
5934 self.advance();
5935 labels.push(s);
5936 }
5937 other => {
5938 return Err(
5939 self.err(alloc::format!("expected enum label string, got {other:?}"))
5940 );
5941 }
5942 }
5943 if matches!(self.peek(), Token::Comma) {
5944 self.advance();
5945 continue;
5946 }
5947 if matches!(self.peek(), Token::RParen) {
5948 self.advance();
5949 break;
5950 }
5951 return Err(self.err(alloc::format!(
5952 "expected , or ) in ENUM label list, got {:?}",
5953 self.peek()
5954 )));
5955 }
5956 if labels.is_empty() {
5957 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5958 }
5959 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5960 name,
5961 kind: crate::ast::TypeKind::Enum { labels },
5962 }))
5963 }
5964
5965 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5966 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5967 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5968 /// consumed.
5969 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5970 let if_not_exists = self.parse_if_not_exists();
5971 let name = self.expect_ident_like()?;
5972 let mut columns: Vec<String> = Vec::new();
5973 if matches!(self.peek(), Token::LParen) {
5974 self.advance();
5975 loop {
5976 let c = self.expect_ident_like()?;
5977 columns.push(c);
5978 if matches!(self.peek(), Token::Comma) {
5979 self.advance();
5980 continue;
5981 }
5982 if matches!(self.peek(), Token::RParen) {
5983 self.advance();
5984 break;
5985 }
5986 return Err(self.err(alloc::format!(
5987 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5988 self.peek()
5989 )));
5990 }
5991 }
5992 if !matches!(self.peek(), Token::As) {
5993 return Err(self.err(alloc::format!(
5994 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5995 self.peek()
5996 )));
5997 }
5998 self.advance();
5999 // v7.39 (round 151) — a WITH-headed body is legal (read-only
6000 // CTEs only; the engine rejects data-modifying ones with PG's
6001 // message). A trailing `WITH [NO] DATA` can't START the body,
6002 // so WITH here heads the query.
6003 let body = if self.peek_is_with_kw() {
6004 self.advance();
6005 self.parse_nested_with_select()?
6006 } else {
6007 let body_stmt = self.parse_select_stmt()?;
6008 let Statement::Select(body) = body_stmt else {
6009 return Err(self.err(alloc::format!(
6010 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6011 )));
6012 };
6013 body
6014 };
6015 // Optional trailing `WITH [NO] DATA`.
6016 let with_data = self.parse_optional_with_data(true)?;
6017 Ok(Statement::CreateMaterializedView(
6018 crate::ast::CreateMaterializedViewStatement {
6019 temporary: false,
6020 name,
6021 if_not_exists,
6022 columns,
6023 body,
6024 with_data,
6025 as_plain_table: false,
6026 },
6027 ))
6028 }
6029
6030 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6031 /// `default_when_absent` is what to return if the tail is
6032 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6033 /// WITH DATA).
6034 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6035 let save = self.pos;
6036 // `WITH` is an Ident (not reserved in the lexer).
6037 let is_with = match self.peek() {
6038 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6039 _ => false,
6040 };
6041 if !is_with {
6042 return Ok(default_when_absent);
6043 }
6044 self.advance();
6045 // Optional `NO`.
6046 let mut with_data = true;
6047 let is_no = match self.peek() {
6048 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6049 _ => false,
6050 };
6051 if is_no {
6052 self.advance();
6053 with_data = false;
6054 }
6055 // Required `DATA` ident.
6056 let is_data = match self.peek() {
6057 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6058 _ => false,
6059 };
6060 if is_data {
6061 self.advance();
6062 Ok(with_data)
6063 } else {
6064 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6065 // parser can interpret it.
6066 self.pos = save;
6067 Ok(default_when_absent)
6068 }
6069 }
6070
6071 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6072 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6073 /// All keyword prefixes have already been consumed; the flags
6074 /// say which were present.
6075 fn parse_create_view_after_keyword(
6076 &mut self,
6077 or_replace: bool,
6078 _materialized_unused: bool,
6079 temporary: bool,
6080 ) -> Result<Statement, ParseError> {
6081 let if_not_exists = self.parse_if_not_exists();
6082 let name = self.expect_ident_like()?;
6083 // Optional `(col, col, …)` rename list.
6084 let mut columns: Vec<String> = Vec::new();
6085 if matches!(self.peek(), Token::LParen) {
6086 self.advance();
6087 loop {
6088 let c = self.expect_ident_like()?;
6089 columns.push(c);
6090 if matches!(self.peek(), Token::Comma) {
6091 self.advance();
6092 continue;
6093 }
6094 if matches!(self.peek(), Token::RParen) {
6095 self.advance();
6096 break;
6097 }
6098 return Err(self.err(alloc::format!(
6099 "expected , or ) in VIEW column list, got {:?}",
6100 self.peek()
6101 )));
6102 }
6103 }
6104 // Required `AS`.
6105 if !matches!(self.peek(), Token::As) {
6106 return Err(self.err(alloc::format!(
6107 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6108 self.peek()
6109 )));
6110 }
6111 self.advance();
6112 // Body: a regular SELECT statement. v7.39 (round 151) — a
6113 // WITH-headed body is legal too (read-only CTEs only; the
6114 // engine rejects data-modifying ones with PG's message).
6115 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6116 // with the check-option clause, so WITH here heads the query.
6117 let body = if self.peek_is_with_kw() {
6118 self.advance();
6119 self.parse_nested_with_select()?
6120 } else {
6121 let body_stmt = self.parse_select_stmt()?;
6122 let Statement::Select(body) = body_stmt else {
6123 return Err(self.err(alloc::format!(
6124 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6125 )));
6126 };
6127 body
6128 };
6129 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6130 // The SELECT parser stops before a trailing WITH, so it lands here.
6131 let check_option = if matches!(self.peek(),
6132 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6133 {
6134 self.advance(); // WITH
6135 let opt = match self.peek() {
6136 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6137 self.advance();
6138 crate::ast::ViewCheckOption::Local
6139 }
6140 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6141 self.advance();
6142 crate::ast::ViewCheckOption::Cascaded
6143 }
6144 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6145 _ => crate::ast::ViewCheckOption::Cascaded,
6146 };
6147 if !matches!(self.peek(),
6148 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6149 {
6150 return Err(self.err(alloc::format!(
6151 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6152 self.peek()
6153 )));
6154 }
6155 self.advance(); // CHECK
6156 if !matches!(self.peek(),
6157 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6158 {
6159 return Err(self.err(alloc::format!(
6160 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6161 self.peek()
6162 )));
6163 }
6164 self.advance(); // OPTION
6165 Some(opt)
6166 } else {
6167 None
6168 };
6169 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6170 name,
6171 or_replace,
6172 if_not_exists,
6173 temporary,
6174 columns,
6175 body,
6176 check_option,
6177 }))
6178 }
6179
6180 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6181 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6182 /// consumed; `temporary` carries whether TEMPORARY was seen.
6183 fn parse_create_sequence_after_keyword(
6184 &mut self,
6185 temporary: bool,
6186 ) -> Result<Statement, ParseError> {
6187 let if_not_exists = self.parse_if_not_exists();
6188 let name = self.expect_ident_like()?;
6189 // Optional `AS data_type`.
6190 let data_type = if matches!(self.peek(), Token::As) {
6191 self.advance();
6192 Some(self.parse_sequence_data_type()?)
6193 } else {
6194 None
6195 };
6196 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6197 Ok(Statement::CreateSequence(
6198 crate::ast::CreateSequenceStatement {
6199 name,
6200 if_not_exists,
6201 temporary,
6202 data_type,
6203 options,
6204 },
6205 ))
6206 }
6207
6208 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6209 /// already been consumed; this is reached after `SEQUENCE`.
6210 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6211 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6212 use crate::ast::AlterDomainAction as A;
6213 let name = self.expect_ident_like()?;
6214 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6215 let kw = match self.peek() {
6216 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6217 Token::Drop => alloc::string::String::from("drop"),
6218 Token::Default => alloc::string::String::from("default"),
6219 other => {
6220 return Err(self.err(alloc::format!(
6221 "expected an ALTER DOMAIN action, got {other:?}"
6222 )));
6223 }
6224 };
6225 let action = match kw.as_str() {
6226 "add" => {
6227 self.advance();
6228 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6229 {
6230 self.advance();
6231 Some(self.expect_ident_like()?)
6232 } else {
6233 None
6234 };
6235 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6236 return Err(self.err(alloc::format!(
6237 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6238 self.peek()
6239 )));
6240 }
6241 self.advance();
6242 if !matches!(self.peek(), Token::LParen) {
6243 return Err(self.err("expected '(' after CHECK".into()));
6244 }
6245 self.advance();
6246 let check = self.parse_expr(0)?;
6247 if !matches!(self.peek(), Token::RParen) {
6248 return Err(self.err("expected ')' after CHECK expression".into()));
6249 }
6250 self.advance();
6251 A::AddConstraint { name: cname, check }
6252 }
6253 "drop" => {
6254 self.advance();
6255 match self.peek() {
6256 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6257 self.advance();
6258 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6259 {
6260 self.advance();
6261 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6262 {
6263 return Err(self.err("expected EXISTS after IF".into()));
6264 }
6265 self.advance();
6266 true
6267 } else {
6268 false
6269 };
6270 let cn = self.expect_ident_like()?;
6271 A::DropConstraint {
6272 name: cn,
6273 if_exists,
6274 }
6275 }
6276 Token::Default => {
6277 self.advance();
6278 A::DropDefault
6279 }
6280 Token::Not => {
6281 self.advance();
6282 if !matches!(self.peek(), Token::Null) {
6283 return Err(self.err("expected NULL after NOT".into()));
6284 }
6285 self.advance();
6286 A::DropNotNull
6287 }
6288 other => {
6289 return Err(self.err(alloc::format!(
6290 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6291 )));
6292 }
6293 }
6294 }
6295 "set" => {
6296 self.advance();
6297 match self.peek() {
6298 Token::Default => {
6299 self.advance();
6300 A::SetDefault(self.parse_expr(0)?)
6301 }
6302 Token::Not => {
6303 self.advance();
6304 if !matches!(self.peek(), Token::Null) {
6305 return Err(self.err("expected NULL after NOT".into()));
6306 }
6307 self.advance();
6308 A::SetNotNull
6309 }
6310 other => {
6311 return Err(self.err(alloc::format!(
6312 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6313 )));
6314 }
6315 }
6316 }
6317 "rename" => {
6318 self.advance();
6319 if !matches!(self.peek(), Token::To) {
6320 return Err(self.err("expected TO after RENAME".into()));
6321 }
6322 self.advance();
6323 A::RenameTo(self.expect_ident_like()?)
6324 }
6325 other => {
6326 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6327 }
6328 };
6329 Ok(Statement::AlterDomain { name, action })
6330 }
6331
6332 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6333 let if_exists = self.parse_if_exists();
6334 let name = self.expect_ident_like()?;
6335 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6336 // the option list (PG allows only one or the other).
6337 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6338 self.advance();
6339 if matches!(self.peek(), Token::To) {
6340 self.advance();
6341 } else {
6342 self.expect_keyword_ident("to")?;
6343 }
6344 let new = self.expect_ident_like()?;
6345 return Ok(Statement::AlterSequence(
6346 crate::ast::AlterSequenceStatement {
6347 name,
6348 if_exists,
6349 options: crate::ast::SequenceOptions::default(),
6350 rename_to: Some(new),
6351 },
6352 ));
6353 }
6354 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6355 Ok(Statement::AlterSequence(
6356 crate::ast::AlterSequenceStatement {
6357 name,
6358 if_exists,
6359 options,
6360 rename_to: None,
6361 },
6362 ))
6363 }
6364
6365 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6366 let kw = self.expect_ident_like()?;
6367 match kw.to_ascii_lowercase().as_str() {
6368 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6369 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6370 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6371 other => Err(self.err(alloc::format!(
6372 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6373 ))),
6374 }
6375 }
6376
6377 fn parse_sequence_options(
6378 &mut self,
6379 allow_restart: bool,
6380 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6381 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6382 let mut opts = SequenceOptions::default();
6383 #[allow(clippy::while_let_loop)]
6384 loop {
6385 // Match an ident; stop at any non-ident token (sentinel,
6386 // semicolon, end of statement).
6387 let kw_lc = match self.peek() {
6388 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6389 _ => break,
6390 };
6391 match kw_lc.as_str() {
6392 "increment" => {
6393 self.advance();
6394 // Optional BY.
6395 if self.peek_is_by() {
6396 self.advance();
6397 }
6398 opts.increment = Some(self.expect_signed_int()?);
6399 }
6400 "minvalue" => {
6401 self.advance();
6402 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6403 }
6404 "maxvalue" => {
6405 self.advance();
6406 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6407 }
6408 "no" => {
6409 self.advance();
6410 let what = self.expect_ident_like()?;
6411 match what.to_ascii_lowercase().as_str() {
6412 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6413 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6414 "cycle" => opts.cycle = Some(false),
6415 other => {
6416 return Err(self.err(alloc::format!(
6417 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6418 )));
6419 }
6420 }
6421 }
6422 "start" => {
6423 self.advance();
6424 // Optional WITH.
6425 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6426 if s.eq_ignore_ascii_case("with"))
6427 {
6428 self.advance();
6429 }
6430 opts.start = Some(self.expect_signed_int()?);
6431 }
6432 "restart" if allow_restart => {
6433 self.advance();
6434 // Optional WITH n; bare RESTART means restart at START.
6435 let mut with_val: Option<i64> = None;
6436 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6437 if s.eq_ignore_ascii_case("with"))
6438 {
6439 self.advance();
6440 with_val = Some(self.expect_signed_int()?);
6441 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6442 with_val = Some(self.expect_signed_int()?);
6443 }
6444 opts.restart = Some(with_val);
6445 }
6446 "cache" => {
6447 self.advance();
6448 opts.cache = Some(self.expect_signed_int()?);
6449 }
6450 "cycle" => {
6451 self.advance();
6452 opts.cycle = Some(true);
6453 }
6454 "owned" => {
6455 self.advance();
6456 match self.peek() {
6457 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6458 self.advance();
6459 }
6460 other => {
6461 return Err(
6462 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6463 );
6464 }
6465 }
6466 // OWNED BY {NONE | tab.col}. Read just one ident
6467 // (NOT expect_ident_like which would auto-strip
6468 // a schema prefix and consume the `.col` we need).
6469 let first = match self.advance() {
6470 Token::Ident(s) | Token::QuotedIdent(s) => s,
6471 other => {
6472 return Err(self.err(alloc::format!(
6473 "expected identifier or NONE after OWNED BY, got {other:?}"
6474 )));
6475 }
6476 };
6477 if first.eq_ignore_ascii_case("none") {
6478 opts.owned_by = Some(SequenceOwnedBy::None);
6479 } else if matches!(self.peek(), Token::Dot) {
6480 self.advance();
6481 let second = match self.advance() {
6482 Token::Ident(s) | Token::QuotedIdent(s) => s,
6483 other => {
6484 return Err(self.err(alloc::format!(
6485 "expected column name after OWNED BY {first}., got {other:?}"
6486 )));
6487 }
6488 };
6489 // v7.17 dump-compat fix — pg_dump emits
6490 // OWNED BY clauses as
6491 // `schema.table.column` (three segments).
6492 // If a third `.<ident>` follows, treat the
6493 // first ident as schema (drop it; SPG is
6494 // single-schema) and the middle / last
6495 // pair as table.column. Otherwise it's
6496 // the two-segment form table.column.
6497 if matches!(self.peek(), Token::Dot) {
6498 self.advance();
6499 let third = match self.advance() {
6500 Token::Ident(s) | Token::QuotedIdent(s) => s,
6501 other => {
6502 return Err(self.err(alloc::format!(
6503 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6504 )));
6505 }
6506 };
6507 let _ = first; // schema prefix discarded
6508 opts.owned_by = Some(SequenceOwnedBy::Column {
6509 table: second,
6510 column: third,
6511 });
6512 } else {
6513 opts.owned_by = Some(SequenceOwnedBy::Column {
6514 table: first,
6515 column: second,
6516 });
6517 }
6518 } else {
6519 return Err(self.err(alloc::format!(
6520 "expected table.column or NONE after OWNED BY, got {first:?}"
6521 )));
6522 }
6523 }
6524 _ => break,
6525 }
6526 }
6527 Ok(opts)
6528 }
6529
6530 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6531 let neg = if matches!(self.peek(), Token::Minus) {
6532 self.advance();
6533 true
6534 } else {
6535 false
6536 };
6537 match self.peek() {
6538 Token::Integer(n) => {
6539 let v = *n;
6540 self.advance();
6541 Ok(if neg { -v } else { v })
6542 }
6543 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6544 }
6545 }
6546
6547 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6548 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6549 /// clause is fully accepted and discarded — SPG always runs
6550 /// constraint checks immediately (single-writer model). The
6551 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6552 /// in either order (per the SQL spec they're independent),
6553 /// though pg_dump always emits them in the canonical
6554 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6555 /// Stops at the first token that isn't part of the clause.
6556 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6557 self.consume_deferrable_clauses_timed().map(|_| ())
6558 }
6559
6560 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6561 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6562 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6563 /// NOT DEFERRABLE and a circular-FK migration could not load.
6564 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6565 let mut deferrable = false;
6566 let mut initially_deferred = false;
6567 loop {
6568 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6569 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6570 self.advance();
6571 deferrable = true;
6572 if self.consume_optional_initially_clause()? {
6573 initially_deferred = true;
6574 }
6575 continue;
6576 }
6577 // `NOT DEFERRABLE` — already worked pre-3.1.
6578 if matches!(self.peek(), Token::Not) {
6579 let look = self.tokens.get(self.pos + 1);
6580 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6581 self.advance(); // NOT
6582 self.advance(); // DEFERRABLE
6583 deferrable = false;
6584 initially_deferred = false;
6585 let _ = self.consume_optional_initially_clause()?;
6586 continue;
6587 }
6588 break;
6589 }
6590 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6591 // accepts this without a leading [NOT] DEFERRABLE
6592 // (the timing keyword alone). pg_dump occasionally
6593 // emits it on FK constraints that inherit timing.
6594 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6595 if self.consume_optional_initially_clause()? {
6596 initially_deferred = true;
6597 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6598 deferrable = true;
6599 }
6600 continue;
6601 }
6602 break;
6603 }
6604 Ok((deferrable, initially_deferred))
6605 }
6606
6607 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6608 /// next token is `INITIALLY`, consume it plus the required
6609 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6610 /// Returns true when the timing seen was `DEFERRED`.
6611 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6612 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6613 return Ok(false);
6614 }
6615 self.advance(); // INITIALLY
6616 match self.advance() {
6617 Token::Ident(s)
6618 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6619 {
6620 Ok(s.eq_ignore_ascii_case("deferred"))
6621 }
6622 other => Err(self.err(alloc::format!(
6623 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6624 ))),
6625 }
6626 }
6627
6628 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6629 /// in its entirety so the parser returns Empty without
6630 /// touching the runtime. The CREATE+PROCEDURE keywords are
6631 /// already consumed; this swallows everything from the
6632 /// procedure name through the matching `END`, including
6633 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6634 /// (DELIMITER `//` makes the script splitter forward the
6635 /// whole block as one statement), `@var` session-variable
6636 /// references, and the trailing terminator.
6637 ///
6638 /// Tracks nesting depth so:
6639 /// BEGIN
6640 /// IF cond THEN
6641 /// BEGIN ... END;
6642 /// END IF;
6643 /// END
6644 /// terminates at the outer END.
6645 fn consume_mysql_routine_body(&mut self) {
6646 // Outer skeleton: name, (...), optional clauses, BEGIN
6647 // <body> END [;]. Scan for the first BEGIN — anything
6648 // before it is signature decoration we don't care about.
6649 // Once inside BEGIN, count up on BEGIN, down on END.
6650 let mut depth: i32 = 0;
6651 let mut started = false;
6652 loop {
6653 match self.peek().clone() {
6654 Token::Begin => {
6655 self.advance();
6656 depth += 1;
6657 started = true;
6658 }
6659 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6660 self.advance();
6661 if started {
6662 depth -= 1;
6663 if depth <= 0 {
6664 // Optional trailing ident (`END IF`,
6665 // `END LOOP`, `END WHILE`, `END CASE`,
6666 // `END label_name`) — eat the next
6667 // ident if present so we don't
6668 // mistake `END IF;` for the outer
6669 // close.
6670 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6671 // If the next token is one of the
6672 // PL/SQL block-closer keywords,
6673 // the END belongs to an inner
6674 // block; bump depth back up.
6675 let is_inner_close = matches!(
6676 self.peek(),
6677 Token::Ident(s) | Token::QuotedIdent(s)
6678 if matches!(
6679 s.to_ascii_lowercase().as_str(),
6680 "if" | "loop" | "while" | "case" | "repeat"
6681 )
6682 );
6683 if is_inner_close {
6684 self.advance();
6685 depth += 1;
6686 continue;
6687 }
6688 }
6689 // Eat optional trailing `;`.
6690 if matches!(self.peek(), Token::Semicolon) {
6691 self.advance();
6692 }
6693 return;
6694 }
6695 }
6696 }
6697 Token::Eof => return,
6698 _ => {
6699 self.advance();
6700 }
6701 }
6702 }
6703 }
6704
6705 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6706 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6707 ///
6708 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6709 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6710 /// ident, or `ident @ ident-or-quoted-string` host form)
6711 /// * `SQL SECURITY {DEFINER|INVOKER}`
6712 ///
6713 /// Each clause may appear at most once but in any order.
6714 /// The hints are pure planner / permission metadata that
6715 /// SPG's view-rewrite engine handles uniformly; we accept
6716 /// and discard. Returns `Ok(())` once a non-clause token is
6717 /// peeked (the caller then checks for the `VIEW` keyword).
6718 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6719 loop {
6720 match self.peek().clone() {
6721 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6722 self.advance(); // ALGORITHM
6723 // Optional `=`. MySQL spec requires it but be
6724 // generous.
6725 if matches!(self.peek(), Token::Eq) {
6726 self.advance();
6727 }
6728 // UNDEFINED / MERGE / TEMPTABLE — accept any
6729 // bare ident; unknown values still parse so
6730 // future MySQL versions don't break.
6731 if matches!(
6732 self.peek(),
6733 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6734 ) {
6735 self.advance();
6736 }
6737 }
6738 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6739 self.advance(); // DEFINER
6740 if matches!(self.peek(), Token::Eq) {
6741 self.advance();
6742 }
6743 // User: quoted string, ident, OR ident @ host
6744 // (host may itself be quoted or bare).
6745 match self.peek().clone() {
6746 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6747 self.advance();
6748 // Optional `@host`.
6749 if matches!(self.peek(), Token::At) {
6750 self.advance();
6751 if matches!(
6752 self.peek(),
6753 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6754 ) {
6755 self.advance();
6756 }
6757 }
6758 }
6759 _ => {}
6760 }
6761 }
6762 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6763 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6764 // when followed by SECURITY — the dispatcher must
6765 // not consume a bare `SQL` token (it's not a
6766 // legal CREATE prefix on its own).
6767 let save = self.pos;
6768 self.advance(); // SQL
6769 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6770 if s2.eq_ignore_ascii_case("security"))
6771 {
6772 self.advance(); // SECURITY
6773 // DEFINER / INVOKER trailing ident.
6774 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6775 self.advance();
6776 }
6777 } else {
6778 // Not a SQL SECURITY clause — roll back and
6779 // bail; the caller will error out cleanly.
6780 self.pos = save;
6781 return Ok(());
6782 }
6783 }
6784 _ => return Ok(()),
6785 }
6786 }
6787 }
6788
6789 fn parse_if_not_exists(&mut self) -> bool {
6790 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6791 {
6792 let save = self.pos;
6793 self.advance();
6794 if matches!(self.peek(), Token::Not) {
6795 self.advance();
6796 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6797 {
6798 self.advance();
6799 return true;
6800 }
6801 }
6802 self.pos = save;
6803 }
6804 false
6805 }
6806
6807 fn parse_if_exists(&mut self) -> bool {
6808 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6809 {
6810 let save = self.pos;
6811 self.advance();
6812 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6813 {
6814 self.advance();
6815 return true;
6816 }
6817 self.pos = save;
6818 }
6819 false
6820 }
6821
6822 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6823 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6824 /// been consumed.
6825 fn parse_create_trigger_after_keyword(
6826 &mut self,
6827 or_replace: bool,
6828 ) -> Result<Statement, ParseError> {
6829 let name = self.expect_ident_like()?;
6830 let timing = {
6831 let ident = self.expect_ident_like()?;
6832 if ident.eq_ignore_ascii_case("before") {
6833 TriggerTiming::Before
6834 } else if ident.eq_ignore_ascii_case("after") {
6835 TriggerTiming::After
6836 } else if ident.eq_ignore_ascii_case("instead") {
6837 let next = self.expect_ident_like()?;
6838 if !next.eq_ignore_ascii_case("of") {
6839 return Err(self.err(alloc::format!(
6840 "expected OF after INSTEAD in trigger timing, got {next:?}"
6841 )));
6842 }
6843 TriggerTiming::InsteadOf
6844 } else {
6845 return Err(self.err(alloc::format!(
6846 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6847 )));
6848 }
6849 };
6850 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6851 // OR is a reserved keyword token (Token::Or), not an Ident.
6852 // v7.13.0 — after an UPDATE event we may optionally see
6853 // `OF col, col, …` (mailrs round-5 G7). Columns are
6854 // captured into `update_columns` once across the whole
6855 // events list; multiple `UPDATE OF` clauses are rejected.
6856 let mut events: Vec<TriggerEvent> = Vec::new();
6857 let mut update_columns: Vec<String> = Vec::new();
6858 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6859 events.push(first_ev);
6860 if !first_cols.is_empty() {
6861 update_columns = first_cols;
6862 }
6863 while matches!(self.peek(), Token::Or) {
6864 self.advance();
6865 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6866 events.push(ev);
6867 if !cols.is_empty() {
6868 if !update_columns.is_empty() {
6869 return Err(
6870 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6871 );
6872 }
6873 update_columns = cols;
6874 }
6875 }
6876 // ON <table>
6877 let tok = self.peek();
6878 let Token::On = tok else {
6879 return Err(self.err(alloc::format!(
6880 "expected ON after trigger events, got {tok:?}"
6881 )));
6882 };
6883 self.advance();
6884 let table = self.expect_ident_like()?;
6885 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6886 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6887 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6888 // the trigger as a plain AFTER trigger (correct for every non-deferred
6889 // use; deferral timing is not yet honoured).
6890 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6891 if s.eq_ignore_ascii_case("from"))
6892 {
6893 self.advance();
6894 let _reftable = self.expect_ident_like()?;
6895 }
6896 self.consume_optional_deferrable_clauses()?;
6897 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6898 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6899 // idents.
6900 if !matches!(self.peek(), Token::For) {
6901 return Err(self.err(alloc::format!(
6902 "expected FOR EACH ROW / STATEMENT, got {:?}",
6903 self.peek()
6904 )));
6905 }
6906 self.advance();
6907 let for_each = {
6908 let e = self.expect_ident_like()?;
6909 if !e.eq_ignore_ascii_case("each") {
6910 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6911 }
6912 let unit = self.expect_ident_like()?;
6913 if unit.eq_ignore_ascii_case("row") {
6914 TriggerForEach::Row
6915 } else if unit.eq_ignore_ascii_case("statement") {
6916 TriggerForEach::Statement
6917 } else {
6918 return Err(self.err(alloc::format!(
6919 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6920 )));
6921 }
6922 };
6923 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6924 let when_condition = if matches!(self.peek(),
6925 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6926 {
6927 self.advance();
6928 Some(self.parse_paren_expr("WHEN")?)
6929 } else {
6930 None
6931 };
6932 // EXECUTE FUNCTION/PROCEDURE name(...)
6933 let exec = self.expect_ident_like()?;
6934 if !exec.eq_ignore_ascii_case("execute") {
6935 return Err(self.err(alloc::format!(
6936 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6937 )));
6938 }
6939 let fn_or_proc = self.expect_ident_like()?;
6940 if !(fn_or_proc.eq_ignore_ascii_case("function")
6941 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6942 {
6943 return Err(self.err(alloc::format!(
6944 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6945 )));
6946 }
6947 let function = self.expect_ident_like()?;
6948 // Optional empty arg list `()`.
6949 if matches!(self.peek(), Token::LParen) {
6950 self.advance();
6951 if !matches!(self.peek(), Token::RParen) {
6952 return Err(self.err(alloc::format!(
6953 "v7.12.4 trigger function calls take no args; got {:?}",
6954 self.peek()
6955 )));
6956 }
6957 self.advance();
6958 }
6959 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6960 name,
6961 or_replace,
6962 timing,
6963 events,
6964 table,
6965 for_each,
6966 function,
6967 update_columns,
6968 when_condition,
6969 }))
6970 }
6971
6972 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6973 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6974 fn parse_create_rule_after_keyword(
6975 &mut self,
6976 or_replace: bool,
6977 ) -> Result<Statement, ParseError> {
6978 let name = self.expect_ident_like()?;
6979 if !matches!(self.peek(), Token::As) {
6980 return Err(self.err(alloc::format!(
6981 "expected AS in CREATE RULE, got {:?}",
6982 self.peek()
6983 )));
6984 }
6985 self.advance();
6986 if !matches!(self.peek(), Token::On) {
6987 return Err(self.err(alloc::format!(
6988 "expected ON in CREATE RULE, got {:?}",
6989 self.peek()
6990 )));
6991 }
6992 self.advance();
6993 let event = self.parse_rule_event()?;
6994 if !matches!(self.peek(), Token::To)
6995 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6996 {
6997 return Err(self.err(alloc::format!(
6998 "expected TO after rule event, got {:?}",
6999 self.peek()
7000 )));
7001 }
7002 self.advance();
7003 let table = self.expect_ident_like()?;
7004 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7005 let when_condition = if matches!(self.peek(), Token::Where) {
7006 self.advance();
7007 Some(self.parse_expr(0)?)
7008 } else {
7009 None
7010 };
7011 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7012 {
7013 return Err(self.err(alloc::format!(
7014 "expected DO in CREATE RULE, got {:?}",
7015 self.peek()
7016 )));
7017 }
7018 self.advance();
7019 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7020 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7021 {
7022 self.advance();
7023 true
7024 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7025 self.advance();
7026 false
7027 } else {
7028 false
7029 };
7030 // `NOTHING` | `( cmd; … )` | `cmd`.
7031 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7032 {
7033 self.advance();
7034 Vec::new()
7035 } else if matches!(self.peek(), Token::LParen) {
7036 self.advance();
7037 let mut cmds = Vec::new();
7038 loop {
7039 cmds.push(self.parse_one_statement()?);
7040 if matches!(self.peek(), Token::Semicolon) {
7041 self.advance();
7042 if matches!(self.peek(), Token::RParen) {
7043 break;
7044 }
7045 continue;
7046 }
7047 break;
7048 }
7049 if !matches!(self.peek(), Token::RParen) {
7050 return Err(self.err(alloc::format!(
7051 "expected ) closing the CREATE RULE command list, got {:?}",
7052 self.peek()
7053 )));
7054 }
7055 self.advance();
7056 cmds
7057 } else {
7058 alloc::vec![self.parse_one_statement()?]
7059 };
7060 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7061 name,
7062 or_replace,
7063 event,
7064 table,
7065 instead,
7066 when_condition,
7067 commands,
7068 }))
7069 }
7070
7071 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7072 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7073 if matches!(self.peek(), Token::Insert) {
7074 self.advance();
7075 return Ok(alloc::string::String::from("INSERT"));
7076 }
7077 if matches!(self.peek(), Token::Select) {
7078 self.advance();
7079 return Ok(alloc::string::String::from("SELECT"));
7080 }
7081 match self.peek() {
7082 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7083 self.advance();
7084 Ok(alloc::string::String::from("UPDATE"))
7085 }
7086 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7087 self.advance();
7088 Ok(alloc::string::String::from("DELETE"))
7089 }
7090 other => Err(self.err(alloc::format!(
7091 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7092 ))),
7093 }
7094 }
7095
7096 /// v7.13.0 — parse one trigger event, then optionally consume
7097 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7098 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7099 fn parse_trigger_event_with_optional_of(
7100 &mut self,
7101 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7102 let ev = self.parse_trigger_event()?;
7103 if !matches!(ev, TriggerEvent::Update) {
7104 return Ok((ev, Vec::new()));
7105 }
7106 // `OF` is a bare ident.
7107 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7108 return Ok((ev, Vec::new()));
7109 }
7110 self.advance(); // OF
7111 let mut cols: Vec<String> = Vec::new();
7112 loop {
7113 cols.push(self.expect_ident_like()?);
7114 if matches!(self.peek(), Token::Comma) {
7115 self.advance();
7116 continue;
7117 }
7118 break;
7119 }
7120 if cols.is_empty() {
7121 return Err(
7122 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7123 );
7124 }
7125 Ok((ev, cols))
7126 }
7127
7128 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7129 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7130 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7131 /// inside the body.
7132 /// Called by [`parse_plpgsql_body`] after the body's tokens
7133 /// have been lexed into this temporary parser.
7134 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7135 // v7.12.6 — optional DECLARE prelude.
7136 let declarations = if matches!(
7137 self.peek(),
7138 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7139 ) {
7140 self.advance();
7141 self.parse_plpgsql_declare_block()?
7142 } else {
7143 Vec::new()
7144 };
7145 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7146 // `BEGIN` transaction-start, but we can reuse the
7147 // reserved Token::Begin since the body is a separate
7148 // lex/parse context).
7149 if !matches!(self.peek(), Token::Begin) {
7150 return Err(self.err(alloc::format!(
7151 "expected BEGIN at start of plpgsql block, got {:?}",
7152 self.peek()
7153 )));
7154 }
7155 self.advance();
7156 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7157 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7158 // body's last statement and the trailing END. When present
7159 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7160 // arms terminated by END.
7161 let exception_handlers = if matches!(
7162 self.peek(),
7163 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7164 ) {
7165 self.advance();
7166 self.parse_plpgsql_exception_handlers()?
7167 } else {
7168 Vec::new()
7169 };
7170 Ok(PlPgSqlBlock {
7171 declarations,
7172 statements,
7173 exception_handlers,
7174 })
7175 }
7176
7177 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7178 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7179 fn parse_plpgsql_exception_handlers(
7180 &mut self,
7181 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7182 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7183 loop {
7184 // Stop at END — the block-level trailing END LOOP / END;
7185 // is handled by the caller.
7186 if matches!(
7187 self.peek(),
7188 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7189 ) {
7190 return Ok(out);
7191 }
7192 // WHEN <cond> [OR <cond>]* THEN <body>
7193 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7194 {
7195 return Err(self.err(alloc::format!(
7196 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7197 self.peek()
7198 )));
7199 }
7200 self.advance();
7201 let mut conditions: Vec<String> = Vec::new();
7202 conditions.push(self.expect_ident_like()?);
7203 while matches!(self.peek(), Token::Or) {
7204 self.advance();
7205 conditions.push(self.expect_ident_like()?);
7206 }
7207 let then_kw = self.expect_ident_like()?;
7208 if !then_kw.eq_ignore_ascii_case("then") {
7209 return Err(self.err(alloc::format!(
7210 "expected THEN after WHEN condition list, got {then_kw:?}"
7211 )));
7212 }
7213 let body = self.parse_plpgsql_stmt_list_until_end()?;
7214 out.push(crate::ast::ExceptionHandler { conditions, body });
7215 }
7216 }
7217
7218 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7219 /// prelude. Caller has already consumed `DECLARE`. We stop
7220 /// reading entries when we hit `BEGIN`.
7221 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7222 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7223 loop {
7224 if matches!(self.peek(), Token::Begin) {
7225 return Ok(out);
7226 }
7227 let name = self.expect_ident_like()?;
7228 // v7.37.20 (20.7) — type inference: if the next token is
7229 // `:=` or `=` (no explicit type), infer from the default
7230 // expression. Otherwise the ident that follows is the
7231 // declared type.
7232 //
7233 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7234 // (PG-standard). SPG parse-accepts and treats identically
7235 // to inference — the eventual runtime value determines
7236 // the local's type, which is faithful to how SPG handles
7237 // untyped locals today (see 20.7). Full compile-time
7238 // catalog lookup queues with v7.40 PL/pgSQL epic.
7239 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7240 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7241 // downstream declaration walker to type the local by
7242 // the runtime type of the default expression.
7243 FunctionArgType::Raw("_infer_".into())
7244 } else {
7245 let ty_token = self.expect_ident_like()?;
7246 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7247 // consume optional `.<ident>` qualifier + `%<KW>`
7248 // suffix. Both qualifier and suffix map to _infer_.
7249 if matches!(self.peek(), Token::Dot) {
7250 self.advance();
7251 let _ = self.expect_ident_like()?;
7252 }
7253 if matches!(self.peek(), Token::Percent) {
7254 self.advance();
7255 // Consume the trailing TYPE / ROWTYPE ident.
7256 let _ = self.expect_ident_like()?;
7257 FunctionArgType::Raw("_infer_".into())
7258 } else {
7259 match map_type_ident_to_column_type_name(&ty_token) {
7260 Some(t) => FunctionArgType::Typed(t),
7261 None => FunctionArgType::Raw(ty_token),
7262 }
7263 }
7264 };
7265 let default = match self.peek() {
7266 Token::ColonEq => {
7267 self.advance();
7268 Some(self.parse_expr(0)?)
7269 }
7270 Token::Eq => {
7271 // PL/pgSQL also accepts `=` for the
7272 // DECLARE default (PG treats them the same
7273 // in this position).
7274 self.advance();
7275 Some(self.parse_expr(0)?)
7276 }
7277 _ => None,
7278 };
7279 // Mandatory `;` between declarations.
7280 if !matches!(self.peek(), Token::Semicolon) {
7281 return Err(self.err(alloc::format!(
7282 "expected ; after DECLARE entry for {name:?}, got {:?}",
7283 self.peek()
7284 )));
7285 }
7286 self.advance();
7287 out.push(PlPgSqlDeclare { name, ty, default });
7288 }
7289 }
7290
7291 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7292 /// the terminating `END;` (or `END IF;` etc — handled by the
7293 /// per-construct sub-parsers). Used by both the outer block
7294 /// and the IF/ELSE branch bodies.
7295 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7296 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7297 loop {
7298 // Allow trailing semicolons + END.
7299 while matches!(self.peek(), Token::Semicolon) {
7300 self.advance();
7301 }
7302 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7303 if matches!(
7304 self.peek(),
7305 Token::Ident(s) | Token::QuotedIdent(s)
7306 if s.eq_ignore_ascii_case("end")
7307 || s.eq_ignore_ascii_case("else")
7308 || s.eq_ignore_ascii_case("elsif")
7309 || s.eq_ignore_ascii_case("elseif")
7310 || s.eq_ignore_ascii_case("exception")
7311 || s.eq_ignore_ascii_case("when")
7312 ) {
7313 return Ok(statements);
7314 }
7315 // Otherwise: one statement, then expect `;` or
7316 // a block-terminator keyword.
7317 let stmt = self.parse_plpgsql_stmt()?;
7318 statements.push(stmt);
7319 match self.peek() {
7320 Token::Semicolon => {
7321 self.advance();
7322 }
7323 Token::Ident(s) | Token::QuotedIdent(s)
7324 if s.eq_ignore_ascii_case("end")
7325 || s.eq_ignore_ascii_case("else")
7326 || s.eq_ignore_ascii_case("elsif")
7327 || s.eq_ignore_ascii_case("elseif")
7328 || s.eq_ignore_ascii_case("exception")
7329 || s.eq_ignore_ascii_case("when") =>
7330 {
7331 // Final statement of the block without `;`.
7332 }
7333 other => {
7334 return Err(self.err(alloc::format!(
7335 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7336 )));
7337 }
7338 }
7339 }
7340 }
7341
7342 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7343 // RETURN keyword?
7344 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7345 {
7346 self.advance();
7347 return self.parse_plpgsql_return();
7348 }
7349 // v7.12.6 — IF block.
7350 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7351 {
7352 self.advance();
7353 return self.parse_plpgsql_if();
7354 }
7355 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7356 // Detected by peeking that token pos+3 is Ident("execute").
7357 if matches!(self.peek(), Token::For)
7358 && matches!(
7359 self.tokens.get(self.pos + 1),
7360 Some(Token::Ident(_) | Token::QuotedIdent(_))
7361 )
7362 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7363 && matches!(
7364 self.tokens.get(self.pos + 3),
7365 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7366 )
7367 {
7368 self.advance(); // FOR
7369 let var = self.expect_ident_like()?;
7370 self.advance(); // IN
7371 self.advance(); // EXECUTE
7372 // Prescan for LOOP at paren depth 0 so parse_expr stops
7373 // before the LOOP keyword (same trick as the bare-SELECT
7374 // ForQuery arm).
7375 let mut depth: i32 = 0;
7376 let mut loop_pos: Option<usize> = None;
7377 let mut scan = self.pos;
7378 while scan < self.tokens.len() {
7379 match self.tokens.get(scan) {
7380 Some(Token::LParen) => depth += 1,
7381 Some(Token::RParen) => depth -= 1,
7382 Some(Token::Ident(s) | Token::QuotedIdent(s))
7383 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7384 {
7385 loop_pos = Some(scan);
7386 break;
7387 }
7388 _ => {}
7389 }
7390 scan += 1;
7391 }
7392 let loop_pos = loop_pos.ok_or_else(|| {
7393 self.err(alloc::format!(
7394 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7395 ))
7396 })?;
7397 let saved_loop = self.tokens[loop_pos].clone();
7398 self.tokens[loop_pos] = Token::Semicolon;
7399 let expr_result = self.parse_expr(0);
7400 self.tokens[loop_pos] = saved_loop;
7401 let sql_expr = expr_result?;
7402 let loop_kw = self.expect_ident_like()?;
7403 if !loop_kw.eq_ignore_ascii_case("loop") {
7404 return Err(self.err(alloc::format!(
7405 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7406 )));
7407 }
7408 let body = self.parse_plpgsql_stmt_list_until_end()?;
7409 let end_kw = self.expect_ident_like()?;
7410 if !end_kw.eq_ignore_ascii_case("end") {
7411 return Err(self.err(alloc::format!(
7412 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7413 )));
7414 }
7415 let loop_kw2 = self.expect_ident_like()?;
7416 if !loop_kw2.eq_ignore_ascii_case("loop") {
7417 return Err(self.err(alloc::format!(
7418 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7419 )));
7420 }
7421 return Ok(PlPgSqlStmt::ForExecute {
7422 var,
7423 sql_expr,
7424 body,
7425 });
7426 }
7427 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7428 //
7429 // Two syntactic forms:
7430 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7431 // FOR var IN (SELECT ...) LOOP ...
7432 //
7433 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7434 // the trailing `LOOP` keyword as a table alias, we prescan
7435 // forward to find LOOP at paren depth 0, splice a fake
7436 // Semicolon at that position (so SELECT parses cleanly),
7437 // then re-splice LOOP back in.
7438 //
7439 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7440 // LOOP directly — no scan required.
7441 if matches!(self.peek(), Token::For)
7442 && matches!(
7443 self.tokens.get(self.pos + 1),
7444 Some(Token::Ident(_) | Token::QuotedIdent(_))
7445 )
7446 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7447 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7448 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7449 {
7450 self.advance(); // FOR
7451 let var = self.expect_ident_like()?;
7452 // IN
7453 self.advance();
7454 let query = if matches!(self.peek(), Token::LParen) {
7455 // Paren-wrapped SELECT.
7456 self.advance();
7457 let inner = self.parse_select_stmt()?;
7458 let Statement::Select(q) = inner else {
7459 return Err(self.err(alloc::format!(
7460 "expected SELECT inside (…), got {:?}",
7461 self.peek()
7462 )));
7463 };
7464 if !matches!(self.peek(), Token::RParen) {
7465 return Err(self.err(alloc::format!(
7466 "expected ')' after FOR-IN-SELECT body, got {:?}",
7467 self.peek()
7468 )));
7469 }
7470 self.advance();
7471 q
7472 } else {
7473 // Bare SELECT: prescan to find the LOOP boundary.
7474 let mut depth: i32 = 0;
7475 let mut loop_pos: Option<usize> = None;
7476 let mut scan = self.pos;
7477 while scan < self.tokens.len() {
7478 match self.tokens.get(scan) {
7479 Some(Token::LParen) => depth += 1,
7480 Some(Token::RParen) => depth -= 1,
7481 Some(Token::Ident(s) | Token::QuotedIdent(s))
7482 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7483 {
7484 loop_pos = Some(scan);
7485 break;
7486 }
7487 _ => {}
7488 }
7489 scan += 1;
7490 }
7491 let loop_pos = loop_pos.ok_or_else(|| {
7492 self.err(alloc::format!(
7493 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7494 ))
7495 })?;
7496 // Swap the LOOP token with a synthetic Semicolon so
7497 // parse_select_stmt stops there, then restore afterward.
7498 let saved_loop = self.tokens[loop_pos].clone();
7499 self.tokens[loop_pos] = Token::Semicolon;
7500 let parse_result = self.parse_select_stmt();
7501 self.tokens[loop_pos] = saved_loop;
7502 let inner = parse_result?;
7503 let Statement::Select(q) = inner else {
7504 return Err(self.err(alloc::format!(
7505 "expected SELECT after FOR <var> IN, got {:?}",
7506 self.peek()
7507 )));
7508 };
7509 q
7510 };
7511 let loop_kw = self.expect_ident_like()?;
7512 if !loop_kw.eq_ignore_ascii_case("loop") {
7513 return Err(self.err(alloc::format!(
7514 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7515 )));
7516 }
7517 let body = self.parse_plpgsql_stmt_list_until_end()?;
7518 let end_kw = self.expect_ident_like()?;
7519 if !end_kw.eq_ignore_ascii_case("end") {
7520 return Err(self.err(alloc::format!(
7521 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7522 )));
7523 }
7524 let loop_kw2 = self.expect_ident_like()?;
7525 if !loop_kw2.eq_ignore_ascii_case("loop") {
7526 return Err(self.err(alloc::format!(
7527 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7528 )));
7529 }
7530 return Ok(PlPgSqlStmt::ForQuery {
7531 var,
7532 query: Box::new(query),
7533 body,
7534 });
7535 }
7536 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7537 // FOR is a reserved keyword token (Token::For).
7538 if matches!(self.peek(), Token::For)
7539 && matches!(
7540 self.tokens.get(self.pos + 1),
7541 Some(Token::Ident(_) | Token::QuotedIdent(_))
7542 )
7543 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7544 {
7545 self.advance(); // FOR
7546 let var = self.expect_ident_like()?;
7547 if !matches!(self.peek(), Token::In) {
7548 return Err(self.err(alloc::format!(
7549 "expected IN after FOR <var>, got {:?}",
7550 self.peek()
7551 )));
7552 }
7553 self.advance();
7554 let reverse = matches!(
7555 self.peek(),
7556 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7557 );
7558 if reverse {
7559 self.advance();
7560 }
7561 let start = self.parse_expr(0)?;
7562 if !matches!(self.peek(), Token::DotDot) {
7563 return Err(self.err(alloc::format!(
7564 "expected '..' between FOR loop bounds, got {:?}",
7565 self.peek()
7566 )));
7567 }
7568 self.advance();
7569 let end = self.parse_expr(0)?;
7570 let loop_kw = self.expect_ident_like()?;
7571 if !loop_kw.eq_ignore_ascii_case("loop") {
7572 return Err(self.err(alloc::format!(
7573 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7574 )));
7575 }
7576 let body = self.parse_plpgsql_stmt_list_until_end()?;
7577 let end_kw = self.expect_ident_like()?;
7578 if !end_kw.eq_ignore_ascii_case("end") {
7579 return Err(self.err(alloc::format!(
7580 "expected END LOOP after FOR body, got {end_kw:?}"
7581 )));
7582 }
7583 let loop_kw2 = self.expect_ident_like()?;
7584 if !loop_kw2.eq_ignore_ascii_case("loop") {
7585 return Err(self.err(alloc::format!(
7586 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7587 )));
7588 }
7589 return Ok(PlPgSqlStmt::ForRange {
7590 var,
7591 start,
7592 end,
7593 reverse,
7594 body,
7595 });
7596 }
7597 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7598 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7599 {
7600 self.advance();
7601 let body = self.parse_plpgsql_stmt_list_until_end()?;
7602 let end_kw = self.expect_ident_like()?;
7603 if !end_kw.eq_ignore_ascii_case("end") {
7604 return Err(self.err(alloc::format!(
7605 "expected END LOOP after LOOP body, got {end_kw:?}"
7606 )));
7607 }
7608 let loop_kw = self.expect_ident_like()?;
7609 if !loop_kw.eq_ignore_ascii_case("loop") {
7610 return Err(self.err(alloc::format!(
7611 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7612 )));
7613 }
7614 return Ok(PlPgSqlStmt::Loop { body });
7615 }
7616 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7617 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7618 {
7619 self.advance();
7620 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7621 {
7622 self.advance();
7623 Some(self.parse_expr(0)?)
7624 } else {
7625 None
7626 };
7627 return Ok(PlPgSqlStmt::Exit { when });
7628 }
7629 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7630 // already-parsed Statement or a runtime-computed SQL string.
7631 // The disambiguator vs the extended-query-protocol `EXECUTE
7632 // <stmt_name>` (which is a top-level Statement, not a
7633 // plpgsql line) is that inside a DO block / trigger body the
7634 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7635 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7636 {
7637 self.advance();
7638 let sql = self.parse_expr(0)?;
7639 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7640 }
7641 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7642 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7643 {
7644 self.advance();
7645 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7646 {
7647 self.advance();
7648 Some(self.parse_expr(0)?)
7649 } else {
7650 None
7651 };
7652 return Ok(PlPgSqlStmt::Continue { when });
7653 }
7654 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7655 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7656 {
7657 self.advance();
7658 let condition = self.parse_expr(0)?;
7659 let loop_kw = self.expect_ident_like()?;
7660 if !loop_kw.eq_ignore_ascii_case("loop") {
7661 return Err(self.err(alloc::format!(
7662 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7663 )));
7664 }
7665 let body = self.parse_plpgsql_stmt_list_until_end()?;
7666 // Expect END LOOP.
7667 let end_kw = self.expect_ident_like()?;
7668 if !end_kw.eq_ignore_ascii_case("end") {
7669 return Err(self.err(alloc::format!(
7670 "expected END LOOP after WHILE body, got {end_kw:?}"
7671 )));
7672 }
7673 let loop_kw2 = self.expect_ident_like()?;
7674 if !loop_kw2.eq_ignore_ascii_case("loop") {
7675 return Err(self.err(alloc::format!(
7676 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7677 )));
7678 }
7679 return Ok(PlPgSqlStmt::While { condition, body });
7680 }
7681 // v7.12.6 — RAISE.
7682 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7683 {
7684 self.advance();
7685 return self.parse_plpgsql_raise();
7686 }
7687 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7688 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7689 {
7690 self.advance();
7691 let condition = self.parse_expr(0)?;
7692 let message = if matches!(self.peek(), Token::Comma) {
7693 self.advance();
7694 Some(self.parse_expr(0)?)
7695 } else {
7696 None
7697 };
7698 return Ok(PlPgSqlStmt::Assert { condition, message });
7699 }
7700 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7701 // "PERFORM is equivalent to SELECT but discards the
7702 // result." Side effects (function calls, RAISE inside
7703 // SQL functions, etc.) still execute. We desugar to
7704 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7705 // existing embedded-statement path handles execution +
7706 // result-discard cleanly. The result is naturally
7707 // discarded because EmbeddedSql doesn't propagate row
7708 // sets back to the plpgsql interpreter.
7709 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7710 {
7711 self.advance();
7712 // Splice a synthetic Token::Select into the stream at
7713 // the current position so parse_select_stmt parses the
7714 // remainder as a normal SELECT body. Token-stream
7715 // surgery mirrors the try_parse_plpgsql_select_into
7716 // pattern used for SELECT … INTO desugaring.
7717 self.tokens.insert(self.pos, Token::Select);
7718 let select = self.parse_select_stmt()?;
7719 let Statement::Select(s) = select else {
7720 return Err(self.err(alloc::format!(
7721 "expected SELECT body after PERFORM, got {:?}",
7722 self.peek()
7723 )));
7724 };
7725 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7726 }
7727 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7728 // plpgsql-specific shape (mailrs round-10 migrate-042).
7729 // PG's SELECT INTO at top-level SQL would CREATE a new
7730 // table; inside plpgsql it ASSIGNS the query result to
7731 // a local variable. We detect the INTO at paren-depth
7732 // 0 between SELECT and the statement boundary; if
7733 // found, split the token stream into "pre-INTO
7734 // projection" + "var" + "post-INTO FROM/WHERE…" and
7735 // rebuild as a SelectInto with a regular SELECT body
7736 // (no INTO clause).
7737 if matches!(self.peek(), Token::Select)
7738 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7739 {
7740 return Ok(PlPgSqlStmt::SelectInto {
7741 var: var_name,
7742 body: Box::new(select_body),
7743 });
7744 }
7745 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7746 // SELECT can appear directly inside a trigger body; we
7747 // recurse into the regular Statement parser, which will
7748 // stop at the trailing `;` (which our caller then
7749 // consumes).
7750 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7751 // also embed ALTER / CREATE / DROP statements; route
7752 // those through the same parser so the DO body parses
7753 // cleanly.
7754 if matches!(self.peek(), Token::Insert)
7755 || matches!(self.peek(), Token::Select)
7756 || matches!(self.peek(), Token::Create)
7757 || matches!(self.peek(), Token::Drop)
7758 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7759 if s.eq_ignore_ascii_case("update")
7760 || s.eq_ignore_ascii_case("delete")
7761 || s.eq_ignore_ascii_case("alter"))
7762 {
7763 let stmt = self.parse_one_statement()?;
7764 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7765 }
7766 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7767 // followed by `:=` and an expression.
7768 let target = self.parse_plpgsql_assign_target()?;
7769 // PL/pgSQL assignment uses `:=`. The lexer represents
7770 // this as a colon followed by `=`; check both shapes.
7771 match self.peek() {
7772 Token::ColonEq => {
7773 self.advance();
7774 }
7775 Token::Colon => {
7776 self.advance();
7777 if !matches!(self.peek(), Token::Eq) {
7778 return Err(self.err(alloc::format!(
7779 "expected := after plpgsql assign target, got `:` then {:?}",
7780 self.peek()
7781 )));
7782 }
7783 self.advance();
7784 }
7785 other => {
7786 return Err(self.err(alloc::format!(
7787 "expected := after plpgsql assign target, got {other:?}"
7788 )));
7789 }
7790 }
7791 let value = self.parse_expr(0)?;
7792 Ok(PlPgSqlStmt::Assign { target, value })
7793 }
7794
7795 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7796 /// [ELSE body] END IF`. `IF` keyword already consumed.
7797 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7798 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7799 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7800 loop {
7801 // <expr> THEN
7802 let cond = self.parse_expr(0)?;
7803 let then_kw = self.expect_ident_like()?;
7804 if !then_kw.eq_ignore_ascii_case("then") {
7805 return Err(self.err(alloc::format!(
7806 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7807 )));
7808 }
7809 let body = self.parse_plpgsql_stmt_list_until_end()?;
7810 branches.push((cond, body));
7811 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7812 match self.peek() {
7813 Token::Ident(s) | Token::QuotedIdent(s)
7814 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7815 {
7816 self.advance();
7817 continue;
7818 }
7819 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7820 self.advance();
7821 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7822 break;
7823 }
7824 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7825 break;
7826 }
7827 other => {
7828 return Err(self.err(alloc::format!(
7829 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7830 )));
7831 }
7832 }
7833 }
7834 // Expect `END IF` (the END keyword is the one we're
7835 // looking at right now).
7836 let end_kw = self.expect_ident_like()?;
7837 if !end_kw.eq_ignore_ascii_case("end") {
7838 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7839 }
7840 let if_kw = self.expect_ident_like()?;
7841 if !if_kw.eq_ignore_ascii_case("if") {
7842 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7843 }
7844 Ok(PlPgSqlStmt::If {
7845 branches,
7846 else_branch,
7847 })
7848 }
7849
7850 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7851 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7852 /// is already consumed.
7853 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7854 let lvl_ident = self.expect_ident_like()?;
7855 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7856 "notice" => RaiseLevel::Notice,
7857 "warning" => RaiseLevel::Warning,
7858 "info" => RaiseLevel::Info,
7859 "log" => RaiseLevel::Log,
7860 "debug" => RaiseLevel::Debug,
7861 "exception" => RaiseLevel::Exception,
7862 other => {
7863 return Err(self.err(alloc::format!(
7864 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7865 )));
7866 }
7867 };
7868 // Message: required for v7.12.6. PG accepts a bare
7869 // RAISE-rethrow form (no message), reserved for future
7870 // RAISE-no-args support.
7871 let Token::String(msg) = self.peek() else {
7872 return Err(self.err(alloc::format!(
7873 "expected RAISE message string, got {:?}",
7874 self.peek()
7875 )));
7876 };
7877 let message = msg.clone();
7878 self.advance();
7879 // Optional comma-separated args (PG `%` format substitution).
7880 let mut args: Vec<Expr> = Vec::new();
7881 while matches!(self.peek(), Token::Comma) {
7882 self.advance();
7883 args.push(self.parse_expr(0)?);
7884 }
7885 Ok(PlPgSqlStmt::Raise {
7886 level,
7887 message,
7888 args,
7889 })
7890 }
7891
7892 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7893 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7894 /// migrate-042). Returns `(rebuilt_select_without_into,
7895 /// var_name)` when the pattern matches; `None` for
7896 /// regular SELECTs (those go through the embedded-SQL
7897 /// path). Token-stream surgery so the rebuilt SELECT
7898 /// parses through the regular `parse_select_stmt`.
7899 #[allow(clippy::too_many_lines)]
7900 fn try_parse_plpgsql_select_into(
7901 &mut self,
7902 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7903 // Scan forward from `self.pos + 1` (past Token::Select)
7904 // for Token::Into at paren-depth 0, stopping at the
7905 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7906 // end the plpgsql statement.
7907 let start = self.pos;
7908 let mut into_pos: Option<usize> = None;
7909 let mut depth: i32 = 0;
7910 let mut i = start + 1;
7911 while i < self.tokens.len() {
7912 match &self.tokens[i] {
7913 Token::LParen => depth += 1,
7914 Token::RParen => depth -= 1,
7915 Token::Semicolon if depth == 0 => break,
7916 Token::Ident(s)
7917 if depth == 0
7918 && (s.eq_ignore_ascii_case("end")
7919 || s.eq_ignore_ascii_case("else")
7920 || s.eq_ignore_ascii_case("elsif")) =>
7921 {
7922 break;
7923 }
7924 Token::Into if depth == 0 => {
7925 into_pos = Some(i);
7926 break;
7927 }
7928 _ => {}
7929 }
7930 i += 1;
7931 }
7932 let Some(into_at) = into_pos else {
7933 return Ok(None);
7934 };
7935 // The token immediately after INTO must be the target
7936 // var ident; anything else (e.g. INSERT INTO table)
7937 // ruled out by the depth-0 check above. Capture it.
7938 let var = match self.tokens.get(into_at + 1) {
7939 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7940 other => {
7941 return Err(self.err(alloc::format!(
7942 "expected variable name after SELECT … INTO, got {other:?}"
7943 )));
7944 }
7945 };
7946 // Find the end of the plpgsql SELECT INTO statement —
7947 // same boundary rules as the depth-0 scan above.
7948 let mut end = into_at + 2;
7949 let mut depth2: i32 = 0;
7950 while end < self.tokens.len() {
7951 match &self.tokens[end] {
7952 Token::LParen => depth2 += 1,
7953 Token::RParen => depth2 -= 1,
7954 Token::Semicolon if depth2 == 0 => break,
7955 Token::Ident(s)
7956 if depth2 == 0
7957 && (s.eq_ignore_ascii_case("end")
7958 || s.eq_ignore_ascii_case("else")
7959 || s.eq_ignore_ascii_case("elsif")) =>
7960 {
7961 break;
7962 }
7963 _ => {}
7964 }
7965 end += 1;
7966 }
7967 // Rebuild a token stream that represents the SELECT
7968 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7969 // post-var tokens up to statement end]. Run the
7970 // regular `parse_select_stmt` against it.
7971 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7972 for j in start..into_at {
7973 rebuilt.push(self.tokens[j].clone());
7974 }
7975 for j in (into_at + 2)..end {
7976 rebuilt.push(self.tokens[j].clone());
7977 }
7978 rebuilt.push(Token::Eof);
7979 let saved_pos = self.pos;
7980 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7981 self.pos = 0;
7982 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7983 if !matches!(self.peek(), Token::Select) {
7984 self.tokens = saved_tokens;
7985 self.pos = saved_pos;
7986 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7987 }
7988 let sel = self.parse_select_stmt();
7989 self.tokens = saved_tokens;
7990 self.pos = end;
7991 let sel = sel?;
7992 let Statement::Select(body) = sel else {
7993 return Err(self.err(alloc::format!(
7994 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7995 )));
7996 };
7997 Ok(Some((body, var)))
7998 }
7999
8000 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8001 // v7.16.1 — read the head token DIRECTLY rather than
8002 // via `expect_ident_like`. The v7.14.0 schema-qualifier
8003 // strip (`public.t` → `t`) inside `expect_ident_like`
8004 // greedily consumes any `ident . ident` pair, which
8005 // silently turned every `NEW.col := …` /
8006 // `OLD.col := …` plpgsql assignment into a Local("col")
8007 // assignment — the head "new"/"old" was eaten as if it
8008 // were a schema name and the Dot was consumed too, so
8009 // this function's own `peek() == Token::Dot` check
8010 // below never fired. Every BEFORE trigger that rewrote
8011 // a NEW cell was a silent no-op for two major releases
8012 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8013 // gate failures were investigated as v7.16.1 backlog.
8014 let head = match self.advance() {
8015 Token::Ident(s) | Token::QuotedIdent(s) => s,
8016 other => {
8017 return Err(self.err(alloc::format!(
8018 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8019 )));
8020 }
8021 };
8022 if matches!(self.peek(), Token::Dot) {
8023 self.advance();
8024 let col = self.expect_ident_like()?;
8025 if head.eq_ignore_ascii_case("new") {
8026 return Ok(AssignTarget::NewColumn(col));
8027 }
8028 if head.eq_ignore_ascii_case("old") {
8029 return Ok(AssignTarget::OldColumn(col));
8030 }
8031 return Err(self.err(alloc::format!(
8032 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8033 got {head:?}.<col>"
8034 )));
8035 }
8036 Ok(AssignTarget::Local(head))
8037 }
8038
8039 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8040 // RETURN NEW / OLD / NULL — bare-ident forms.
8041 match self.peek() {
8042 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8043 self.advance();
8044 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8045 }
8046 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8047 self.advance();
8048 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8049 }
8050 Token::Null => {
8051 self.advance();
8052 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8053 }
8054 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8055 // per PL/pgSQL convention.
8056 Token::Semicolon => {
8057 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8058 }
8059 _ => {}
8060 }
8061 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8062 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8063 // caller-visible effect (blocks don't return sets), so we
8064 // desugar it identically to PERFORM: parse the SELECT (or
8065 // EXECUTE dynamic) as embedded SQL that runs for side
8066 // effects and discards the result. RETURN NEXT <expr>
8067 // (single-row accumulator) queues with v7.40 SETOF function
8068 // infrastructure.
8069 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8070 // and keep going.
8071 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8072 {
8073 self.advance();
8074 let e = self.parse_expr(0)?;
8075 return Ok(PlPgSqlStmt::ReturnNext(e));
8076 }
8077 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8078 {
8079 self.advance();
8080 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8081 // rows go to the set, like the static form. It used to desugar to a
8082 // bare ExecuteDynamic, whose result was DISCARDED.
8083 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8084 {
8085 self.advance();
8086 let sql = self.parse_expr(0)?;
8087 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8088 }
8089 // Bare RETURN QUERY <select>. If the current token is
8090 // not already SELECT (e.g., the user wrote `RETURN QUERY
8091 // <projection> FROM ...` in a shorthand — rare but PG
8092 // accepts a bare projection here), splice one in. Same
8093 // trick as PERFORM.
8094 if !matches!(self.peek(), Token::Select) {
8095 self.tokens.insert(self.pos, Token::Select);
8096 }
8097 let select = self.parse_select_stmt()?;
8098 let Statement::Select(s) = select else {
8099 return Err(self.err(alloc::format!(
8100 "expected SELECT body after RETURN QUERY, got {:?}",
8101 self.peek()
8102 )));
8103 };
8104 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8105 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8106 // in a SETOF function is the entire answer thrown away.
8107 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8108 }
8109 // Fall through: parse a full expression.
8110 let e = self.parse_expr(0)?;
8111 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8112 }
8113
8114 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8115 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8116 // are ident-shaped (the parser keys off case-insensitive
8117 // match — same shape used by the top-level Update / Delete
8118 // dispatchers at parse_one_statement).
8119 if matches!(self.peek(), Token::Insert) {
8120 self.advance();
8121 return Ok(TriggerEvent::Insert);
8122 }
8123 match self.peek() {
8124 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8125 self.advance();
8126 Ok(TriggerEvent::Update)
8127 }
8128 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8129 self.advance();
8130 Ok(TriggerEvent::Delete)
8131 }
8132 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8133 self.advance();
8134 Ok(TriggerEvent::Truncate)
8135 }
8136 other => Err(self.err(alloc::format!(
8137 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8138 ))),
8139 }
8140 }
8141
8142 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8143 /// - (no clause) → implicit `FOR ALL TABLES`
8144 /// - `FOR ALL TABLES`
8145 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8146 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8147 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8148 /// REJECTS the bare plural (`invalid publication object list`,
8149 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8150 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8151 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8152 let name = self.expect_ident_or_string()?;
8153 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8154 // shape so existing publications keep parsing identically.
8155 let scope = if matches!(self.peek(), Token::For) {
8156 self.advance();
8157 if matches!(self.peek(), Token::All) {
8158 self.advance();
8159 if !matches!(self.peek(), Token::Tables) {
8160 return Err(self.err(format!(
8161 "expected TABLES after FOR ALL, got {:?}",
8162 self.peek()
8163 )));
8164 }
8165 self.advance();
8166 if matches!(self.peek(), Token::Except) {
8167 self.advance();
8168 let tables = self.parse_publication_table_list()?;
8169 PublicationScope::AllTablesExcept(tables)
8170 } else {
8171 PublicationScope::AllTables
8172 }
8173 } else if matches!(self.peek(), Token::Table) {
8174 self.advance();
8175 let tables = self.parse_publication_table_list()?;
8176 PublicationScope::ForTables(tables)
8177 } else if matches!(self.peek(), Token::Tables) {
8178 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8179 // plural (`FOR TABLES t`) is REJECTED (`invalid
8180 // publication object list`); TABLES only pairs with
8181 // `IN SCHEMA`. The old arm accepted it on an
8182 // unverifiable "PG 19 accepts both" claim.
8183 self.advance();
8184 if !matches!(self.peek(), Token::In) {
8185 return Err(self.err(alloc::string::String::from(
8186 "invalid publication object list",
8187 )));
8188 }
8189 self.advance();
8190 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8191 return Err(self.err(format!(
8192 "expected SCHEMA after FOR TABLES IN, got {:?}",
8193 self.peek()
8194 )));
8195 }
8196 self.advance();
8197 let schema = self.expect_ident_or_string()?;
8198 PublicationScope::TablesInSchema(schema)
8199 } else {
8200 return Err(self.err(format!(
8201 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8202 self.peek()
8203 )));
8204 }
8205 } else {
8206 PublicationScope::AllTables
8207 };
8208 Ok(Statement::CreatePublication(CreatePublicationStatement {
8209 name,
8210 scope,
8211 }))
8212 }
8213
8214 /// v6.1.3 — Comma-separated identifier list for the publication
8215 /// FOR-clause. Requires at least one entry; empty list is a
8216 /// parse error (PG behaviour). Quoted idents are accepted; the
8217 /// names round-trip through `Display` as `quote_ident(name)`.
8218 ///
8219 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8220 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8221 /// pg_dump output. SPG's publication state today is per-table
8222 /// only (matching the pre-PG-15 surface); the col list + WHERE
8223 /// are parsed so dumps load through and the table name reaches
8224 /// `PublicationScope::ForTables`, but the filter is not enforced
8225 /// at publish time. Re-open when a customer dogfood gate
8226 /// requires per-row-filter or column-subset publish semantics
8227 /// (which gates on persistent slot state landing first, 21.12).
8228 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8229 let first = self.parse_publication_table_entry()?;
8230 let mut out = alloc::vec![first];
8231 while matches!(self.peek(), Token::Comma) {
8232 self.advance();
8233 out.push(self.parse_publication_table_entry()?);
8234 }
8235 Ok(out)
8236 }
8237
8238 /// One table entry inside a FOR TABLE clause:
8239 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8240 /// Returns just the table name; the column list + WHERE predicate
8241 /// are consumed and discarded per the parse-accept-discard
8242 /// commitment above.
8243 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8244 let name = self.expect_ident_like()?;
8245 // Optional column list — `(col, col, …)`.
8246 if matches!(self.peek(), Token::LParen) {
8247 self.advance();
8248 // Empty parens are a PG error too; require ≥ 1 column.
8249 let _ = self.expect_ident_like()?;
8250 while matches!(self.peek(), Token::Comma) {
8251 self.advance();
8252 let _ = self.expect_ident_like()?;
8253 }
8254 if !matches!(self.peek(), Token::RParen) {
8255 return Err(self.err(alloc::format!(
8256 "expected ')' to close publication column list, got {:?}",
8257 self.peek()
8258 )));
8259 }
8260 self.advance();
8261 }
8262 // Optional row filter — `WHERE (predicate)`.
8263 if matches!(self.peek(), Token::Where) {
8264 self.advance();
8265 if !matches!(self.peek(), Token::LParen) {
8266 return Err(self.err(alloc::format!(
8267 "expected '(' after WHERE in publication row filter, got {:?}",
8268 self.peek()
8269 )));
8270 }
8271 self.advance();
8272 let _ = self.parse_expr(0)?;
8273 if !matches!(self.peek(), Token::RParen) {
8274 return Err(self.err(alloc::format!(
8275 "expected ')' to close publication WHERE filter, got {:?}",
8276 self.peek()
8277 )));
8278 }
8279 self.advance();
8280 }
8281 Ok(name)
8282 }
8283
8284 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8285 /// CONNECTION '<conn>'
8286 /// PUBLICATION <pub> [, <pub> ...]`.
8287 ///
8288 /// The clause order is fixed (CONNECTION first, then
8289 /// PUBLICATION) to match PG. No WITH-options accepted in
8290 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8291 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8292 let name = self.expect_ident_or_string()?;
8293 if !matches!(self.peek(), Token::Connection) {
8294 return Err(self.err(format!(
8295 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8296 self.peek()
8297 )));
8298 }
8299 self.advance();
8300 let conn_str = self.expect_string_literal()?;
8301 if !matches!(self.peek(), Token::Publication) {
8302 return Err(self.err(format!(
8303 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8304 self.peek()
8305 )));
8306 }
8307 self.advance();
8308 // Reuse the publication FOR-list parser shape: at least one
8309 // identifier, comma-separated.
8310 let first = self.expect_ident_like()?;
8311 let mut publications = alloc::vec![first];
8312 while matches!(self.peek(), Token::Comma) {
8313 self.advance();
8314 publications.push(self.expect_ident_like()?);
8315 }
8316 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8317 name,
8318 conn_str,
8319 publications,
8320 }))
8321 }
8322
8323 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8324 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8325 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8326 /// that fit `u64`.
8327 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8328 /// qualifier is a *namespace* the app owns (`app.user_id`,
8329 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8330 /// to discard. So parse the raw segments here instead of
8331 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8332 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8333 /// a single segment and round-trip unchanged.
8334 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8335 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8336 loop {
8337 let seg = match self.advance() {
8338 Token::Ident(s) | Token::QuotedIdent(s) => s,
8339 other if unreserved_keyword_text(&other).is_some() => {
8340 unreserved_keyword_text(&other).unwrap()
8341 }
8342 other => {
8343 return Err(ParseError {
8344 message: format!("expected parameter name, got {other:?}"),
8345 token_pos: self.consumed_pos(),
8346 });
8347 }
8348 };
8349 parts.push(seg);
8350 if matches!(self.peek(), Token::Dot) {
8351 self.advance();
8352 continue;
8353 }
8354 break;
8355 }
8356 Ok(parts.join(".").to_ascii_lowercase())
8357 }
8358
8359 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8360 Self::parse_set_value_inner(self)
8361 }
8362
8363 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8364 match self.advance() {
8365 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8366 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8367 Ok(crate::ast::SetValue::Default)
8368 }
8369 Token::Ident(s) | Token::QuotedIdent(s) => {
8370 let mut accum = s;
8371 while matches!(self.peek(), Token::Dot) {
8372 self.advance();
8373 let next = self.expect_ident_like()?;
8374 accum.push('.');
8375 accum.push_str(&next);
8376 }
8377 Ok(crate::ast::SetValue::Ident(accum))
8378 }
8379 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8380 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8381 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8382 // spellings that lex as keyword tokens, not idents:
8383 // `SET standard_conforming_strings = on` is in every
8384 // pg_dump preamble (`off` already lexes as an ident).
8385 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8386 // DEFAULT lexes as its keyword token, so the ident arm above
8387 // never saw it and the everyday reset form was a syntax error.
8388 Token::Default => Ok(crate::ast::SetValue::Default),
8389 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8390 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8391 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8392 // v7.14.0 — MySQL session/user variable RHS
8393 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8394 // Wrap as Ident so the SET handler can record it; the
8395 // engine treats `@VAR` / `@@VAR` values as opaque
8396 // strings.
8397 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8398 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8399 // is the common MySQL preamble shape. Allow a `+` or
8400 // `-` prefix on negative numerics for parity with PG
8401 // (some param defaults are negative).
8402 Token::Minus => match self.advance() {
8403 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8404 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8405 other => Err(self.err(format!(
8406 "expected numeric after `-` in SET value, got {other:?}"
8407 ))),
8408 },
8409 other => Err(self.err(format!(
8410 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8411 ))),
8412 }
8413 }
8414
8415 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8416 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8417 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8418 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8419 /// present). Modes are comma-separated per PG; SPG also
8420 /// accepts space-separated for tolerance. READ ONLY / WRITE
8421 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8422 /// surface but not behaviorally honoured today).
8423 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8424 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8425 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8426 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8427 /// session default rather than forcing READ COMMITTED.
8428 fn parse_isolation_level_clauses(
8429 &mut self,
8430 ) -> Result<crate::ast::TransactionModes, ParseError> {
8431 let mut level = IsolationLevel::default();
8432 let mut have_level = false;
8433 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8434 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8435 let mut read_only: Option<bool> = None;
8436 loop {
8437 // ISOLATION LEVEL …
8438 let saw_isolation =
8439 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8440 if saw_isolation {
8441 self.advance(); // ISOLATION
8442 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8443 return Err(self.err(alloc::format!(
8444 "expected LEVEL after ISOLATION, got {:?}",
8445 self.peek()
8446 )));
8447 }
8448 self.advance(); // LEVEL
8449 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8450 let w1 = self
8451 .expect_ident_like()
8452 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8453 let lc = w1.to_ascii_lowercase();
8454 level = match lc.as_str() {
8455 "serializable" => IsolationLevel::Serializable,
8456 "repeatable" => {
8457 // Expect READ
8458 let w2 = self
8459 .expect_ident_like()
8460 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8461 if !w2.eq_ignore_ascii_case("read") {
8462 return Err(self.err(alloc::format!(
8463 "expected READ after REPEATABLE, got {w2:?}"
8464 )));
8465 }
8466 IsolationLevel::RepeatableRead
8467 }
8468 "read" => {
8469 let w2 = self
8470 .expect_ident_like()
8471 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8472 match w2.to_ascii_lowercase().as_str() {
8473 "committed" => IsolationLevel::ReadCommitted,
8474 "uncommitted" => IsolationLevel::ReadUncommitted,
8475 other => {
8476 return Err(self.err(alloc::format!(
8477 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8478 )));
8479 }
8480 }
8481 }
8482 other => {
8483 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8484 }
8485 };
8486 have_level = true;
8487 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8488 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8489 // read "parsed, not behaviorally honoured", and it was
8490 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8491 // opened an ordinary read-write transaction and accepted
8492 // every write in it.
8493 self.advance();
8494 match self.peek().clone() {
8495 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8496 self.advance();
8497 read_only = Some(true);
8498 }
8499 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8500 self.advance();
8501 read_only = Some(false);
8502 }
8503 other => {
8504 return Err(self.err(alloc::format!(
8505 "expected ONLY or WRITE after READ, got {other:?}"
8506 )));
8507 }
8508 }
8509 } else if matches!(self.peek(), Token::Not) {
8510 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8511 self.advance();
8512 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8513 return Err(self.err(alloc::format!(
8514 "expected DEFERRABLE after NOT, got {:?}",
8515 self.peek()
8516 )));
8517 }
8518 self.advance();
8519 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8520 {
8521 self.advance();
8522 } else {
8523 break;
8524 }
8525 // Optional comma between modes.
8526 if matches!(self.peek(), Token::Comma) {
8527 self.advance();
8528 }
8529 }
8530 Ok(crate::ast::TransactionModes {
8531 isolation: have_level.then_some(level),
8532 read_only,
8533 })
8534 }
8535
8536 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8537 // FOR is a v6.1.2-reserved keyword (Token::For). The
8538 // other two are bare idents — they've never needed lexer
8539 // support and we keep it that way.
8540 if !matches!(self.peek(), Token::For) {
8541 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8542 }
8543 self.advance();
8544 self.expect_keyword_ident("wal")?;
8545 self.expect_keyword_ident("position")?;
8546 let pos = self.expect_u64_literal()?;
8547 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8548 {
8549 self.advance();
8550 self.expect_keyword_ident("timeout")?;
8551 Some(self.expect_u64_literal()?)
8552 } else {
8553 None
8554 };
8555 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8556 }
8557
8558 /// v6.1.7 helper — consume a `Token::Integer` and check it
8559 /// fits `u64`. WAL positions and millisecond timeouts are
8560 /// non-negative.
8561 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8562 match self.advance() {
8563 Token::Integer(n) if n >= 0 => Ok(n as u64),
8564 Token::Integer(n) => Err(ParseError {
8565 message: format!("expected non-negative integer, got {n}"),
8566 token_pos: self.consumed_pos(),
8567 }),
8568 other => Err(ParseError {
8569 message: format!("expected integer literal, got {other:?}"),
8570 token_pos: self.consumed_pos(),
8571 }),
8572 }
8573 }
8574
8575 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8576 /// ROLE '<role>' (defaults to readonly). All string slots accept
8577 /// either a quoted ident or a quoted string literal.
8578 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8579 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8580 ///
8581 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8582 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8583 /// wire role) still parses — it is a different axis from the PG attributes.
8584 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8585 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8586 /// or RESET, so the plain attribute forms keep their old path.
8587 fn peeks_db_role_setting(&self) -> bool {
8588 let mut i = self.pos + 1; // past the object's name
8589 let word = |p: usize| -> Option<String> {
8590 match self.tokens.get(p) {
8591 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8592 Some(Token::In) => Some(String::from("in")),
8593 _ => None,
8594 }
8595 };
8596 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8597 i += 3; // IN DATABASE <name>
8598 }
8599 matches!(word(i).as_deref(), Some("set" | "reset"))
8600 }
8601
8602 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8603 use crate::ast::SetDbRoleSettingStatement;
8604 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8605 // identifier, so the ordinary name reader refuses it. Same trap
8606 // as TABLE / INDEX / FULL / DEFAULT before it.
8607 let name = if matches!(self.peek(), Token::All) {
8608 self.advance();
8609 String::from("all")
8610 } else {
8611 self.expect_ident_or_string()?
8612 };
8613 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8614 let all = name.eq_ignore_ascii_case("all");
8615 let (mut database, mut role) = if is_database {
8616 (Some(name), None)
8617 } else if all {
8618 (None, None)
8619 } else {
8620 (None, Some(name))
8621 };
8622 if matches!(self.peek(), Token::In) {
8623 self.advance();
8624 self.advance(); // DATABASE
8625 database = Some(self.expect_ident_or_string()?);
8626 }
8627 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8628 self.advance(); // SET | RESET
8629 if resetting && matches!(self.peek(), Token::All) {
8630 self.advance();
8631 self.consume_until_statement_boundary();
8632 return Ok(Statement::SetDbRoleSetting(Box::new(
8633 SetDbRoleSettingStatement {
8634 database,
8635 role,
8636 param: None,
8637 value: None,
8638 },
8639 )));
8640 }
8641 let param = self.expect_ident_like()?;
8642 let value = if resetting {
8643 None
8644 } else {
8645 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8646 // KEYWORD, so the ident-only check missed it and consumed
8647 // the word itself as the value — the same trap as ALL, one
8648 // clause over.
8649 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8650 self.advance();
8651 }
8652 Some(self.take_guc_value())
8653 };
8654 self.consume_until_statement_boundary();
8655 Ok(Statement::SetDbRoleSetting(Box::new(
8656 SetDbRoleSettingStatement {
8657 database,
8658 role,
8659 param: Some(param),
8660 value,
8661 },
8662 )))
8663 }
8664
8665 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8666 /// a quoted literal loses its quotes, a bare word or number does not.
8667 fn take_guc_value(&mut self) -> String {
8668 match self.advance() {
8669 Token::String(s) => s,
8670 Token::Integer(n) => format!("{n}"),
8671 Token::Float(f) => format!("{f}"),
8672 Token::Ident(s) | Token::QuotedIdent(s) => s,
8673 other => format!("{other:?}"),
8674 }
8675 }
8676
8677 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8678 let name = self.expect_ident_or_string()?;
8679 if self.peek_keyword_ident("with") {
8680 self.advance();
8681 }
8682 let mut password = String::new();
8683 let mut role = String::new();
8684 let mut login: Option<bool> = None;
8685 let mut inherit: Option<bool> = None;
8686 let mut superuser: Option<bool> = None;
8687 // Not a `while let`: the pattern would borrow `self` across the
8688 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8689 #[allow(clippy::while_let_loop)]
8690 loop {
8691 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8692 break;
8693 };
8694 match w.to_ascii_lowercase().as_str() {
8695 "password" => {
8696 self.advance();
8697 password = self.expect_string_literal()?;
8698 }
8699 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8700 // is the same slot.
8701 "encrypted" => {
8702 self.advance();
8703 self.expect_keyword_ident("password")?;
8704 password = self.expect_string_literal()?;
8705 }
8706 "login" => {
8707 self.advance();
8708 login = Some(true);
8709 }
8710 "nologin" => {
8711 self.advance();
8712 login = Some(false);
8713 }
8714 "inherit" => {
8715 self.advance();
8716 inherit = Some(true);
8717 }
8718 "noinherit" => {
8719 self.advance();
8720 inherit = Some(false);
8721 }
8722 "superuser" => {
8723 self.advance();
8724 superuser = Some(true);
8725 }
8726 "nosuperuser" => {
8727 self.advance();
8728 superuser = Some(false);
8729 }
8730 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8731 "role" => {
8732 self.advance();
8733 role = self.expect_string_literal()?;
8734 }
8735 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8736 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8737 // accepted and ignored so a pg_dump role block restores. They
8738 // gate capabilities SPG does not have.
8739 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8740 | "noreplication" | "bypassrls" | "nobypassrls" => {
8741 self.advance();
8742 }
8743 "connection" => {
8744 self.advance();
8745 self.expect_keyword_ident("limit")?;
8746 self.advance(); // the number
8747 }
8748 "valid" => {
8749 self.advance();
8750 self.expect_keyword_ident("until")?;
8751 self.expect_string_literal()?;
8752 }
8753 _ => break,
8754 }
8755 }
8756 if role.is_empty() {
8757 role = "readonly".to_string();
8758 }
8759 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8760 name,
8761 password,
8762 role,
8763 login,
8764 inherit,
8765 superuser,
8766 is_user,
8767 }))
8768 }
8769
8770 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8771 /// consumed the USING / WITH CHECK keyword.
8772 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8773 if !matches!(self.peek(), Token::LParen) {
8774 return Err(self.err(alloc::format!(
8775 "expected '(' after {clause}, got {:?}",
8776 self.peek()
8777 )));
8778 }
8779 self.advance();
8780 let e = self.parse_expr(0)?;
8781 if !matches!(self.peek(), Token::RParen) {
8782 return Err(self.err(alloc::format!(
8783 "expected ')' to close {clause}, got {:?}",
8784 self.peek()
8785 )));
8786 }
8787 self.advance();
8788 Ok(e)
8789 }
8790
8791 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8792 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8793 let mut roles = Vec::new();
8794 loop {
8795 roles.push(self.expect_ident_like()?);
8796 if matches!(self.peek(), Token::Comma) {
8797 self.advance();
8798 } else {
8799 break;
8800 }
8801 }
8802 Ok(roles)
8803 }
8804
8805 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8806 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8807 /// `CREATE POLICY`.
8808 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8809 use crate::ast::PolicyCmd;
8810 let name = self.expect_ident_like()?;
8811 if !matches!(self.peek(), Token::On) {
8812 return Err(self.err(alloc::format!(
8813 "expected ON after CREATE POLICY name, got {:?}",
8814 self.peek()
8815 )));
8816 }
8817 self.advance();
8818 let table = self.expect_ident_like()?;
8819
8820 let mut permissive = true;
8821 if matches!(self.peek(), Token::As) {
8822 self.advance();
8823 let w = self.expect_ident_like()?;
8824 permissive = if w.eq_ignore_ascii_case("permissive") {
8825 true
8826 } else if w.eq_ignore_ascii_case("restrictive") {
8827 false
8828 } else {
8829 return Err(self.err(alloc::format!(
8830 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8831 )));
8832 };
8833 }
8834
8835 let mut cmd = PolicyCmd::All;
8836 if matches!(self.peek(), Token::For) {
8837 self.advance();
8838 cmd = self.parse_policy_cmd()?;
8839 }
8840
8841 let mut roles = Vec::new();
8842 if matches!(self.peek(), Token::To) {
8843 self.advance();
8844 roles = self.parse_policy_roles()?;
8845 }
8846
8847 let mut using = None;
8848 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8849 {
8850 self.advance();
8851 using = Some(self.parse_paren_expr("USING")?);
8852 }
8853
8854 let mut with_check = None;
8855 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8856 {
8857 self.advance();
8858 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8859 {
8860 return Err(self.err(alloc::format!(
8861 "expected CHECK after WITH, got {:?}",
8862 self.peek()
8863 )));
8864 }
8865 self.advance();
8866 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8867 }
8868
8869 // Clause-per-command matrix (PG wording).
8870 match cmd {
8871 PolicyCmd::Insert => {
8872 if using.is_some() {
8873 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8874 }
8875 }
8876 PolicyCmd::Select | PolicyCmd::Delete => {
8877 if with_check.is_some() {
8878 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8879 }
8880 }
8881 PolicyCmd::Update | PolicyCmd::All => {}
8882 }
8883
8884 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8885 name,
8886 table,
8887 permissive,
8888 cmd,
8889 roles,
8890 using,
8891 with_check,
8892 }))
8893 }
8894
8895 /// v7.39 (RLS) — the command word after `FOR`.
8896 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8897 use crate::ast::PolicyCmd;
8898 match self.peek().clone() {
8899 Token::All => {
8900 self.advance();
8901 Ok(PolicyCmd::All)
8902 }
8903 Token::Select => {
8904 self.advance();
8905 Ok(PolicyCmd::Select)
8906 }
8907 Token::Insert => {
8908 self.advance();
8909 Ok(PolicyCmd::Insert)
8910 }
8911 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8912 self.advance();
8913 Ok(PolicyCmd::Update)
8914 }
8915 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8916 self.advance();
8917 Ok(PolicyCmd::Delete)
8918 }
8919 other => Err(self.err(alloc::format!(
8920 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8921 ))),
8922 }
8923 }
8924
8925 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8926 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8927 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8928 let name = self.expect_ident_like()?;
8929 if !matches!(self.peek(), Token::On) {
8930 return Err(self.err(alloc::format!(
8931 "expected ON after ALTER POLICY name, got {:?}",
8932 self.peek()
8933 )));
8934 }
8935 self.advance();
8936 let table = self.expect_ident_like()?;
8937
8938 // RENAME TO new
8939 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8940 {
8941 self.advance();
8942 if !matches!(self.peek(), Token::To) {
8943 return Err(self.err(alloc::format!(
8944 "expected TO after RENAME, got {:?}",
8945 self.peek()
8946 )));
8947 }
8948 self.advance();
8949 let new = self.expect_ident_like()?;
8950 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8951 name,
8952 table,
8953 rename_to: Some(new),
8954 roles: None,
8955 using: None,
8956 with_check: None,
8957 }));
8958 }
8959
8960 let mut roles = None;
8961 if matches!(self.peek(), Token::To) {
8962 self.advance();
8963 roles = Some(self.parse_policy_roles()?);
8964 }
8965 let mut using = None;
8966 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8967 {
8968 self.advance();
8969 using = Some(self.parse_paren_expr("USING")?);
8970 }
8971 let mut with_check = None;
8972 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8973 {
8974 self.advance();
8975 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8976 {
8977 return Err(self.err(alloc::format!(
8978 "expected CHECK after WITH, got {:?}",
8979 self.peek()
8980 )));
8981 }
8982 self.advance();
8983 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8984 }
8985 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8986 name,
8987 table,
8988 rename_to: None,
8989 roles,
8990 using,
8991 with_check,
8992 }))
8993 }
8994
8995 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8996 /// `DROP POLICY`.
8997 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8998 let if_exists = self.consume_if_exists();
8999 let name = self.expect_ident_like()?;
9000 if !matches!(self.peek(), Token::On) {
9001 return Err(self.err(alloc::format!(
9002 "expected ON after DROP POLICY name, got {:?}",
9003 self.peek()
9004 )));
9005 }
9006 self.advance();
9007 let table = self.expect_ident_like()?;
9008 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9009 name,
9010 table,
9011 if_exists,
9012 }))
9013 }
9014}
9015fn wrap_from_leaves(
9016 e: &mut Expr,
9017 names: &[String],
9018 make: &dyn Fn(Expr) -> Expr,
9019 refs: &dyn Fn(&Expr) -> bool,
9020) {
9021 if let Expr::Column(c) = e {
9022 if c.qualifier
9023 .as_deref()
9024 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9025 {
9026 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9027 *e = make(taken);
9028 }
9029 return;
9030 }
9031 match e {
9032 Expr::Binary { lhs, rhs, .. } => {
9033 wrap_from_leaves(lhs, names, make, refs);
9034 wrap_from_leaves(rhs, names, make, refs);
9035 }
9036 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9037 wrap_from_leaves(expr, names, make, refs)
9038 }
9039 Expr::FunctionCall { args, .. } => {
9040 for a in args.iter_mut() {
9041 wrap_from_leaves(a, names, make, refs);
9042 }
9043 }
9044 Expr::Case {
9045 operand,
9046 branches,
9047 else_branch,
9048 } => {
9049 if let Some(o) = operand.as_deref_mut() {
9050 wrap_from_leaves(o, names, make, refs);
9051 }
9052 for (w, t) in branches.iter_mut() {
9053 wrap_from_leaves(w, names, make, refs);
9054 wrap_from_leaves(t, names, make, refs);
9055 }
9056 if let Some(el) = else_branch.as_deref_mut() {
9057 wrap_from_leaves(el, names, make, refs);
9058 }
9059 }
9060 // Compound variants the walk doesn't decompose: keep the
9061 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9062 // a source table, so nothing regresses.
9063 other => {
9064 if refs(other) {
9065 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9066 *other = make(taken);
9067 }
9068 }
9069 }
9070}
9071
9072/// v7.39 (round 241) — does this expression reference any of the FROM /
9073/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9074/// lowerings)?
9075fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9076 match e {
9077 Expr::Column(c) => c
9078 .qualifier
9079 .as_deref()
9080 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9081 Expr::Binary { lhs, rhs, .. } => {
9082 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9083 }
9084 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9085 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9086 Expr::Case {
9087 operand,
9088 branches,
9089 else_branch,
9090 } => {
9091 operand
9092 .as_deref()
9093 .is_some_and(|o| expr_refs_tables(o, names))
9094 || branches
9095 .iter()
9096 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9097 || else_branch
9098 .as_deref()
9099 .is_some_and(|el| expr_refs_tables(el, names))
9100 }
9101 _ => false,
9102 }
9103}
9104
9105impl Parser {
9106 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9107 /// Caller already consumed the leading `UPDATE` ident.
9108 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9109 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9110 /// after the target name has been read. `JOIN` is a reserved token;
9111 /// the qualifiers are bare idents.
9112 fn peek_is_update_join_start(&self) -> bool {
9113 match self.peek() {
9114 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9115 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9116 Token::Join
9117 | Token::Inner
9118 | Token::Left
9119 | Token::Right
9120 | Token::Cross
9121 | Token::Full => true,
9122 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9123 Token::Ident(s) | Token::QuotedIdent(s) => {
9124 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9125 }
9126 _ => false,
9127 }
9128 }
9129
9130 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9131 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9132 /// expression on the right, and `:=` as a second spelling of `=`.
9133 ///
9134 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9135 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9136 /// and holding this loop's `Vec` + `String` locals there overflowed the
9137 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9138 #[inline(never)]
9139 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9140 let mut assigns: Vec<(String, Expr)> = Vec::new();
9141 let mut settings: Vec<(String, Expr)> = Vec::new();
9142 loop {
9143 // v7.39 (round 554) — a plain NAME here is a session
9144 // setting, not a user variable. mysqldump writes the two in
9145 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9146 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9147 // changes it — and this refused the mixture outright, so no
9148 // dump could be restored past its preamble.
9149 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9150 self.advance();
9151 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9152 return Err(self.err(alloc::format!(
9153 "expected `=` after {name}, got {:?}",
9154 self.peek()
9155 )));
9156 }
9157 self.advance();
9158 let value = self.parse_expr(0)?;
9159 settings.push((name.to_ascii_lowercase(), value));
9160 if matches!(self.peek(), Token::Comma) {
9161 self.advance();
9162 continue;
9163 }
9164 break;
9165 }
9166 let Token::SessionVar(raw) = self.peek().clone() else {
9167 return Err(self.err(alloc::format!(
9168 "expected a user variable after SET, got {:?}",
9169 self.peek()
9170 )));
9171 };
9172 if raw.starts_with("@@") {
9173 return Err(self.err(alloc::string::String::from(
9174 "cannot mix `@@` settings with `@` user variables in one SET",
9175 )));
9176 }
9177 self.advance();
9178 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9179 return Err(self.err(alloc::format!(
9180 "expected `=` or `:=` after {raw}, got {:?}",
9181 self.peek()
9182 )));
9183 }
9184 self.advance();
9185 let value = self.parse_expr(0)?;
9186 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9187 if matches!(self.peek(), Token::Comma) {
9188 self.advance();
9189 continue;
9190 }
9191 break;
9192 }
9193 Ok(Statement::SetUserVars(assigns, settings))
9194 }
9195
9196 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9197 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9198 // NAMED `only` until now, which failed on `relation "only" does
9199 // not exist`. The lookahead is what keeps a table actually
9200 // called `only` working: the keyword is only a keyword when a
9201 // TABLE NAME follows it — and `SET` arrives as an identifier
9202 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9203 // for the table and die on the `=`. Measured by the pin.
9204 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9205 if s.eq_ignore_ascii_case("only"))
9206 && matches!(
9207 self.tokens.get(self.pos + 1),
9208 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9209 );
9210 if only {
9211 self.advance();
9212 }
9213 let table = self.expect_ident_like()?;
9214 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9215 // bare spelling; a bare identifier that is the SET keyword itself
9216 // is the clause, not an alias.
9217 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9218 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9219 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9220 // following JOIN a syntax error.
9221 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9222 let alias = if matches!(self.peek(), Token::As) {
9223 self.advance();
9224 Some(self.expect_ident_like()?)
9225 } else {
9226 match self.peek() {
9227 Token::Ident(s) | Token::QuotedIdent(s)
9228 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9229 {
9230 let a = s.clone();
9231 self.advance();
9232 Some(a)
9233 }
9234 _ => None,
9235 }
9236 };
9237 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9238 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9239 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9240 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9241 // The FIRST table is the mutation target and the rest are sources —
9242 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9243 // SPG already lowers onto correlated subqueries. So rewind, let
9244 // `parse_from_clause` read the whole list (it handles aliases, comma
9245 // lists, and every JOIN form), then peel the target off the front.
9246 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9247 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9248 {
9249 // NOTE: `advance()` destroys the tokens it returns
9250 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9251 // is NOT possible — the tail is read forward, once, through the
9252 // same grammar `parse_from_clause` uses after its primary.
9253 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9254 let mut joins = self.parse_from_joins(&target_qual)?;
9255 if joins.is_empty() {
9256 return Err(self.err(alloc::string::String::from(
9257 "multi-table UPDATE needs at least one source table",
9258 )));
9259 }
9260 let head = joins.remove(0);
9261 // A LEFT join keeps every target row (the unmatched ones see NULL
9262 // on the source side), so it must NOT get the EXISTS row filter
9263 // the inner / comma forms use.
9264 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9265 let src = FromClause {
9266 primary: head.table,
9267 joins,
9268 };
9269 (Some(src), head.on, outer)
9270 } else {
9271 (None, None, false)
9272 };
9273 self.expect_keyword_ident("set")?;
9274 let mut assignments = Vec::new();
9275 loop {
9276 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9277 // …)` — the parenthesized multi-assignment. Expressions
9278 // assign positionally; a subquery RHS clones per column
9279 // keeping only the Nth projection item.
9280 if matches!(self.peek(), Token::LParen) {
9281 self.advance();
9282 let mut cols = alloc::vec![self.expect_ident_like()?];
9283 while matches!(self.peek(), Token::Comma) {
9284 self.advance();
9285 cols.push(self.expect_ident_like()?);
9286 }
9287 if !matches!(self.peek(), Token::RParen) {
9288 return Err(self.err(format!(
9289 "expected ')' after SET column list, got {:?}",
9290 self.peek()
9291 )));
9292 }
9293 self.advance();
9294 if !matches!(self.peek(), Token::Eq) {
9295 return Err(self.err(format!(
9296 "expected `=` after SET column list, got {:?}",
9297 self.peek()
9298 )));
9299 }
9300 self.advance();
9301 if !matches!(self.peek(), Token::LParen) {
9302 return Err(self.err(format!(
9303 "expected '(' after SET (…) =, got {:?}",
9304 self.peek()
9305 )));
9306 }
9307 self.advance();
9308 if matches!(self.peek(), Token::Select) {
9309 let inner = match self.parse_select_stmt()? {
9310 Statement::Select(s) => s,
9311 other => {
9312 return Err(self.err(alloc::format!(
9313 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9314 )));
9315 }
9316 };
9317 if !matches!(self.peek(), Token::RParen) {
9318 return Err(self.err(format!(
9319 "expected ')' after SET subquery, got {:?}",
9320 self.peek()
9321 )));
9322 }
9323 self.advance();
9324 if inner.items.len() != cols.len() {
9325 return Err(self.err(alloc::format!(
9326 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9327 cols.len(),
9328 inner.items.len()
9329 )));
9330 }
9331 for (i, col) in cols.into_iter().enumerate() {
9332 let mut sub = inner.clone();
9333 sub.items = alloc::vec![sub.items[i].clone()];
9334 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9335 }
9336 } else {
9337 let mut exprs = alloc::vec![self.parse_expr(0)?];
9338 while matches!(self.peek(), Token::Comma) {
9339 self.advance();
9340 exprs.push(self.parse_expr(0)?);
9341 }
9342 if !matches!(self.peek(), Token::RParen) {
9343 return Err(self.err(format!(
9344 "expected ')' after SET row values, got {:?}",
9345 self.peek()
9346 )));
9347 }
9348 self.advance();
9349 if exprs.len() != cols.len() {
9350 return Err(self.err(alloc::format!(
9351 "SET (…) = (…) arity mismatch: {} columns, {} values",
9352 cols.len(),
9353 exprs.len()
9354 )));
9355 }
9356 for (col, e) in cols.into_iter().zip(exprs) {
9357 assignments.push((col, e));
9358 }
9359 }
9360 if matches!(self.peek(), Token::Comma) {
9361 self.advance();
9362 continue;
9363 }
9364 break;
9365 }
9366 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9367 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9368 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9369 // `public.` dump qualifiers), so the qualifier has to be read off
9370 // the token stream first — otherwise `SET b.v = 888` would write
9371 // to the TARGET table's `v` while naming a source table, a
9372 // silent-wrong. A qualifier naming a SOURCE table means a
9373 // multi-TARGET update — mutating two tables in one statement —
9374 // which SPG does not model, so it is refused loudly.
9375 let set_qual: Option<String> = if mysql_from.is_some()
9376 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9377 {
9378 match self.peek() {
9379 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9380 _ => None,
9381 }
9382 } else {
9383 None
9384 };
9385 let col = self.expect_ident_like()?;
9386 if let Some(q) = set_qual {
9387 let names_target = q.eq_ignore_ascii_case(&table)
9388 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9389 if !names_target {
9390 return Err(self.err(alloc::format!(
9391 "multi-table UPDATE can only assign to its first table \
9392 ({table}); `{q}.{col}` targets another table"
9393 )));
9394 }
9395 }
9396 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9397 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9398 // `__column_default` marker lowering just below). PG assigns to the
9399 // i-th (1-based) element, NULL-padding when i exceeds the length.
9400 if matches!(self.peek(), Token::LBracket) {
9401 self.advance();
9402 let index = self.parse_expr(0)?;
9403 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9404 // (and the open `arr[lo:]`), lowered to
9405 // `__array_assign_slice`. Only the single-subscript form
9406 // parsed before, so a slice assignment was a syntax error.
9407 let mut slice_hi: Option<Option<Expr>> = None;
9408 if matches!(self.peek(), Token::Colon) {
9409 self.advance();
9410 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9411 None
9412 } else {
9413 Some(self.parse_expr(0)?)
9414 });
9415 }
9416 if !matches!(self.peek(), Token::RBracket) {
9417 return Err(self.err(format!(
9418 "expected `]` after array subscript in UPDATE SET, got {:?}",
9419 self.peek()
9420 )));
9421 }
9422 self.advance();
9423 if !matches!(self.peek(), Token::Eq) {
9424 return Err(self.err(format!(
9425 "expected `=` after array subscript in UPDATE SET, got {:?}",
9426 self.peek()
9427 )));
9428 }
9429 self.advance();
9430 let value = self.parse_expr(0)?;
9431 // PG merges several subscript writes to the same column into one
9432 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9433 // assignment to `col` rather than each overwriting the original.
9434 let existing = assignments.iter().position(|(c, _)| c == &col);
9435 let base = match existing {
9436 Some(i) => assignments[i].1.clone(),
9437 None => Expr::Column(ColumnName {
9438 qualifier: None,
9439 name: col.clone(),
9440 }),
9441 };
9442 let assigned = match slice_hi {
9443 None => Expr::FunctionCall {
9444 name: "__array_assign".to_string(),
9445 args: alloc::vec![base, index, value],
9446 },
9447 Some(hi) => Expr::FunctionCall {
9448 name: "__array_assign_slice".to_string(),
9449 args: alloc::vec![
9450 base,
9451 index,
9452 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9453 value,
9454 ],
9455 },
9456 };
9457 match existing {
9458 Some(i) => assignments[i].1 = assigned,
9459 None => assignments.push((col, assigned)),
9460 }
9461 if matches!(self.peek(), Token::Comma) {
9462 self.advance();
9463 continue;
9464 }
9465 break;
9466 }
9467 if !matches!(self.peek(), Token::Eq) {
9468 return Err(self.err(format!(
9469 "expected `=` after column name in UPDATE SET, got {:?}",
9470 self.peek()
9471 )));
9472 }
9473 self.advance();
9474 // `SET col = DEFAULT` — the column's declared default;
9475 // rides out as a marker call the update executor
9476 // resolves against the schema.
9477 let value = if matches!(self.peek(), Token::Default) {
9478 self.advance();
9479 Expr::FunctionCall {
9480 name: "__column_default".to_string(),
9481 args: Vec::new(),
9482 }
9483 } else {
9484 self.parse_expr(0)?
9485 };
9486 assignments.push((col, value));
9487 if matches!(self.peek(), Token::Comma) {
9488 self.advance();
9489 continue;
9490 }
9491 break;
9492 }
9493 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9494 // update. Lowers onto the correlated-subquery machinery:
9495 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9496 // and each assignment that references a FROM-list table
9497 // wraps into a correlated scalar subquery
9498 // (SELECT expr FROM src WHERE cond). Equivalent for the
9499 // unique-join shape (the overwhelmingly common one); a
9500 // multi-match, which PG resolves by arbitrary pick,
9501 // surfaces as a scalar-subquery cardinality error instead
9502 // of a silent arbitrary result.
9503 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9504 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9505 // the SAME lowering below. Both spellings together is not legal in
9506 // either dialect.
9507 let from_clause = if let Some(fc) = mysql_from {
9508 if matches!(self.peek(), Token::From) {
9509 return Err(self.err(alloc::string::String::from(
9510 "multi-table UPDATE already names its sources; drop the FROM clause",
9511 )));
9512 }
9513 Some(fc)
9514 } else if matches!(self.peek(), Token::From) {
9515 self.advance();
9516 Some(self.parse_from_clause()?)
9517 } else {
9518 None
9519 };
9520 let where_ = if matches!(self.peek(), Token::Where) {
9521 self.advance();
9522 Some(self.parse_expr(0)?)
9523 } else {
9524 None
9525 };
9526 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9527 // and the TARGET-row filter are NOT the same predicate once a LEFT
9528 // join is involved:
9529 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9530 // one conjunction, and the whole thing filters target rows via
9531 // EXISTS.
9532 // * LEFT join: only the ON predicate belongs inside the source
9533 // subquery. The WHERE still filters TARGET rows (with source
9534 // columns read through the correlated subquery, which yields NULL
9535 // for an unmatched row — exactly LEFT-join semantics).
9536 // Round 420 folded ON into WHERE unconditionally and then dropped the
9537 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9538 // WHERE a.id > 1` updated EVERY row.
9539 let sub_where = match (mysql_on.clone(), where_.clone()) {
9540 _ if mysql_outer => mysql_on.clone(),
9541 (Some(on), Some(w)) => Some(Expr::Binary {
9542 lhs: Box::new(on),
9543 op: crate::ast::BinOp::And,
9544 rhs: Box::new(w),
9545 }),
9546 (Some(on), None) => Some(on),
9547 (None, w) => w,
9548 };
9549 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9550 // has no such clause on UPDATE, so this is accepted only under the
9551 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9552 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9553 let mut returning = self.parse_optional_returning()?;
9554 // v7.39 (round 533) — kept for the engine, which can resolve the
9555 // UNQUALIFIED leaves this lowering has to leave alone.
9556 let from_sources = from_clause.as_ref().map(|fc| {
9557 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9558 from: fc.clone(),
9559 sub_where: sub_where.clone(),
9560 })
9561 });
9562 let (assignments, where_) = if let Some(fc) = from_clause {
9563 let names: Vec<String> = core::iter::once(&fc.primary)
9564 .chain(fc.joins.iter().map(|j| &j.table))
9565 .flat_map(|t| {
9566 t.alias
9567 .clone()
9568 .into_iter()
9569 .chain(core::iter::once(t.name.clone()))
9570 })
9571 .collect();
9572 let refs_list = |e: &Expr| -> bool {
9573 fn walk(e: &Expr, names: &[String]) -> bool {
9574 match e {
9575 Expr::Column(c) => c
9576 .qualifier
9577 .as_deref()
9578 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9579 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9580 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9581 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9582 Expr::Case {
9583 operand,
9584 branches,
9585 else_branch,
9586 } => {
9587 operand.as_deref().is_some_and(|o| walk(o, names))
9588 || branches
9589 .iter()
9590 .any(|(w, t)| walk(w, names) || walk(t, names))
9591 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9592 }
9593 _ => false,
9594 }
9595 }
9596 walk(e, &names)
9597 };
9598 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9599 locking: None,
9600 ctes: Vec::new(),
9601 distinct: false,
9602 distinct_on: Vec::new(),
9603 items,
9604 from: Some(fc.clone()),
9605 where_: sub_where.clone(),
9606 group_by: None,
9607 group_by_all: false,
9608 having: None,
9609 unions: Vec::new(),
9610 order_by: Vec::new(),
9611 limit: None,
9612 offset: None,
9613 limit_with_ties: false,
9614 window_check_exprs: Vec::new(),
9615 };
9616 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9617 // assignment RHS with a correlated scalar subquery, instead of
9618 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9619 // column reference (`SET v = v + u.bonus`, where `v` is the target
9620 // table's column) inside a subquery whose FROM only has the source
9621 // table, so the unqualified `v` resolved against the source and
9622 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9623 // context — where they belong — fixes it; only the source columns
9624 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9625 // compound variants the leaf-walk doesn't decompose.
9626 let make_subq = |inner: Expr| {
9627 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9628 expr: inner,
9629 alias: None,
9630 }])))
9631 };
9632 let assignments = assignments
9633 .into_iter()
9634 .map(|(col, mut expr)| {
9635 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9636 (col, expr)
9637 })
9638 .collect();
9639 let exists = Expr::Exists {
9640 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9641 expr: Expr::Literal(Literal::Integer(1)),
9642 alias: None,
9643 }])),
9644 negated: false,
9645 };
9646 // v7.39 (round 241) — RETURNING may reference the FROM-list
9647 // tables too (`RETURNING emp.id, dept.name`); the same
9648 // leaf-to-correlated-subquery lowering the assignments get.
9649 // Without it the qualifier died at eval with "unknown table
9650 // qualifier". (RETURNING was parsed before this block — the
9651 // lowering is a pure AST transformation.)
9652 if let Some(items) = returning.as_mut() {
9653 for item in items.iter_mut() {
9654 if let SelectItem::Expr { expr, .. } = item {
9655 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9656 }
9657 }
9658 }
9659 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9660 // EVERY matching target row: it gets no EXISTS filter, but the
9661 // caller's WHERE still applies, with source columns read through
9662 // the correlated subquery (NULL when unmatched — LEFT-join
9663 // semantics). `sub_where` above already excluded the WHERE from
9664 // the source subquery for this case.
9665 if mysql_outer {
9666 let mut outer = where_;
9667 if let Some(w) = outer.as_mut() {
9668 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9669 }
9670 (assignments, outer)
9671 } else {
9672 (assignments, Some(exists))
9673 }
9674 } else {
9675 (assignments, where_)
9676 };
9677 Ok(Statement::Update(crate::ast::UpdateStatement {
9678 ctes: Vec::new(),
9679 table,
9680 only,
9681 alias,
9682 assignments,
9683 from_sources,
9684 where_,
9685 order_limit: update_order_limit,
9686 returning,
9687 }))
9688 }
9689
9690 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9691 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9692 /// clause and its meaning are identical, so both call this rather than
9693 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9694 /// legal. PG has no such clause on either statement, so it is read only
9695 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9696 /// errors.
9697 ///
9698 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9699 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9700 /// stack in round 430.
9701 #[inline(never)]
9702 fn parse_mysql_dml_order_limit(
9703 &mut self,
9704 what: &str,
9705 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9706 if !self.mysql_dialect {
9707 return Ok(None);
9708 }
9709 let order_by = self.parse_order_by_keys()?;
9710 let limit = if matches!(self.peek(), Token::Limit) {
9711 self.advance();
9712 let tok = self.advance();
9713 let Token::Integer(n) = tok else {
9714 return Err(self.err(alloc::format!(
9715 "expected integer after {what} LIMIT, got {tok:?}"
9716 )));
9717 };
9718 // MySQL rejects the `LIMIT offset, count` form here — only a
9719 // single row count is legal on a DML statement.
9720 if matches!(self.peek(), Token::Comma) {
9721 return Err(self.err(alloc::format!(
9722 "{what} LIMIT takes a row count, not an offset"
9723 )));
9724 }
9725 let n = u32::try_from(n)
9726 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9727 Some(n)
9728 } else {
9729 None
9730 };
9731 if order_by.is_empty() && limit.is_none() {
9732 return Ok(None);
9733 }
9734 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9735 order_by,
9736 limit,
9737 })))
9738 }
9739
9740 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9741 /// the leading `DELETE` ident.
9742 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9743 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9744 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9745 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9746 // parse here; it reaches the existing USING path with the target
9747 // repeated in the list, which the source-list peel below handles.)
9748 // More than one name is a multi-TARGET delete, which SPG does not
9749 // model; it is refused rather than half-applied.
9750 let mysql_pre_target: Option<String> =
9751 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9752 let first = self.expect_ident_like()?;
9753 if matches!(self.peek(), Token::Comma) {
9754 return Err(self.err(alloc::format!(
9755 "multi-table DELETE can only delete from one table; \
9756 `DELETE {first}, …` names several"
9757 )));
9758 }
9759 Some(first)
9760 } else {
9761 None
9762 };
9763 if !matches!(self.peek(), Token::From) {
9764 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9765 }
9766 self.advance();
9767 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9768 // lookahead as the UPDATE spelling.
9769 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9770 if s.eq_ignore_ascii_case("only"))
9771 && matches!(
9772 self.tokens.get(self.pos + 1),
9773 Some(Token::Ident(_) | Token::QuotedIdent(_))
9774 );
9775 if only {
9776 self.advance();
9777 }
9778 let table = self.expect_ident_like()?;
9779 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9780 // spelling must not swallow the clause keywords that can follow
9781 // the target.
9782 let alias = if matches!(self.peek(), Token::As) {
9783 self.advance();
9784 Some(self.expect_ident_like()?)
9785 } else {
9786 match self.peek() {
9787 Token::Ident(s) | Token::QuotedIdent(s)
9788 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9789 {
9790 let a = s.clone();
9791 self.advance();
9792 Some(a)
9793 }
9794 _ => None,
9795 }
9796 };
9797 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9798 // through the SAME join grammar the FROM clause uses (see the
9799 // `advance()`-destroys-tokens note on `parse_from_joins`).
9800 let mut mysql_on: Option<Expr> = None;
9801 let mut mysql_outer = false;
9802 let mysql_using = if mysql_pre_target.is_some()
9803 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9804 {
9805 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9806 let mut joins = self.parse_from_joins(&target_qual)?;
9807 if joins.is_empty() {
9808 return Err(self.err(alloc::string::String::from(
9809 "multi-table DELETE needs at least one source table",
9810 )));
9811 }
9812 let head = joins.remove(0);
9813 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9814 mysql_on = head.on;
9815 Some(FromClause {
9816 primary: head.table,
9817 joins,
9818 })
9819 } else {
9820 None
9821 };
9822 // The pre-FROM target must be the table the FROM names (or its
9823 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9824 // is not the scan target.
9825 if let Some(t) = &mysql_pre_target {
9826 let names_target = t.eq_ignore_ascii_case(&table)
9827 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9828 if !names_target {
9829 return Err(self.err(alloc::format!(
9830 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9831 )));
9832 }
9833 }
9834 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9835 // delete. Same lowering as UPDATE … FROM: the WHERE
9836 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9837 // target row by the correlated machinery.
9838 let using_clause = if let Some(fc) = mysql_using {
9839 Some(fc)
9840 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9841 self.advance();
9842 let mut fc = self.parse_from_clause()?;
9843 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9844 // repeats the TARGET as the first USING entry (PG's spelling
9845 // lists only the extra sources). Peel it so the source subquery
9846 // does not re-scan — and shadow — the target table.
9847 let primary_is_target =
9848 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9849 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9850 let head = fc.joins.remove(0);
9851 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9852 mysql_on = head.on;
9853 fc = FromClause {
9854 primary: head.table,
9855 joins: fc.joins,
9856 };
9857 }
9858 Some(fc)
9859 } else {
9860 None
9861 };
9862 let where_ = if matches!(self.peek(), Token::Where) {
9863 self.advance();
9864 Some(self.parse_expr(0)?)
9865 } else {
9866 None
9867 };
9868 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9869 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9870 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9871 let mut returning = self.parse_optional_returning()?;
9872 let where_ = if let Some(fc) = using_clause {
9873 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9874 // a USING-table reference in RETURNING becomes a correlated
9875 // scalar subquery over the USING list.
9876 let names: Vec<String> = core::iter::once(&fc.primary)
9877 .chain(fc.joins.iter().map(|j| &j.table))
9878 .flat_map(|t| {
9879 t.alias
9880 .clone()
9881 .into_iter()
9882 .chain(core::iter::once(t.name.clone()))
9883 })
9884 .collect();
9885 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9886 // join filters the SOURCE subquery on the ON predicate alone and
9887 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9888 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9889 // rows); every other form folds ON and WHERE into one EXISTS.
9890 let sub_where = match (mysql_on.clone(), where_.clone()) {
9891 _ if mysql_outer => mysql_on.clone(),
9892 (Some(on), Some(w)) => Some(Expr::Binary {
9893 lhs: Box::new(on),
9894 op: crate::ast::BinOp::And,
9895 rhs: Box::new(w),
9896 }),
9897 (Some(on), None) => Some(on),
9898 (None, w) => w,
9899 };
9900 let exists_where = sub_where.clone();
9901 let sub_fc = fc.clone();
9902 let make_subq = move |leaf: Expr| -> Expr {
9903 Expr::ScalarSubquery(Box::new(SelectStatement {
9904 locking: None,
9905 ctes: Vec::new(),
9906 distinct: false,
9907 distinct_on: Vec::new(),
9908 items: alloc::vec![SelectItem::Expr {
9909 expr: leaf,
9910 alias: None,
9911 }],
9912 from: Some(sub_fc.clone()),
9913 where_: sub_where.clone(),
9914 group_by: None,
9915 group_by_all: false,
9916 having: None,
9917 unions: Vec::new(),
9918 order_by: Vec::new(),
9919 limit: None,
9920 offset: None,
9921 limit_with_ties: false,
9922 window_check_exprs: Vec::new(),
9923 }))
9924 };
9925 let refs = |e: &Expr| expr_refs_tables(e, &names);
9926 if let Some(items) = returning.as_mut() {
9927 for item in items.iter_mut() {
9928 if let SelectItem::Expr { expr, .. } = item {
9929 wrap_from_leaves(expr, &names, &make_subq, &refs);
9930 }
9931 }
9932 }
9933 // A LEFT join deletes the target rows the WHERE selects, reading
9934 // source columns through the correlated subquery (NULL when
9935 // unmatched); no EXISTS row filter.
9936 if mysql_outer {
9937 let mut outer = where_;
9938 if let Some(w) = outer.as_mut() {
9939 wrap_from_leaves(w, &names, &make_subq, &refs);
9940 }
9941 outer
9942 } else {
9943 Some(Expr::Exists {
9944 subquery: Box::new(SelectStatement {
9945 locking: None,
9946 ctes: Vec::new(),
9947 distinct: false,
9948 distinct_on: Vec::new(),
9949 items: alloc::vec![SelectItem::Expr {
9950 expr: Expr::Literal(Literal::Integer(1)),
9951 alias: None,
9952 }],
9953 from: Some(fc),
9954 where_: exists_where,
9955 group_by: None,
9956 group_by_all: false,
9957 having: None,
9958 unions: Vec::new(),
9959 order_by: Vec::new(),
9960 limit: None,
9961 offset: None,
9962 limit_with_ties: false,
9963 window_check_exprs: Vec::new(),
9964 }),
9965 negated: false,
9966 })
9967 }
9968 } else {
9969 where_
9970 };
9971 Ok(Statement::Delete(crate::ast::DeleteStatement {
9972 ctes: Vec::new(),
9973 table,
9974 only,
9975 alias,
9976 where_,
9977 order_limit: delete_order_limit,
9978 returning,
9979 }))
9980 }
9981
9982 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9983 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9984 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9985 /// keyword. v7.17 surface:
9986 /// * source: table reference (subquery source is a follow-up)
9987 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9988 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9989 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9990 /// order
9991 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9992 // INTO
9993 let is_into_kw = matches!(self.peek(), Token::Into)
9994 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9995 if !is_into_kw {
9996 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9997 }
9998 self.advance();
9999 let target = self.expect_ident_like()?;
10000 // Optional alias — bare ident before USING.
10001 let target_alias = match self.peek() {
10002 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10003 Some(self.expect_ident_like()?)
10004 }
10005 _ => None,
10006 };
10007 // USING
10008 let is_using_kw = matches!(
10009 self.peek(),
10010 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10011 );
10012 if !is_using_kw {
10013 return Err(self.err(format!(
10014 "expected USING after MERGE INTO target, got {:?}",
10015 self.peek()
10016 )));
10017 }
10018 self.advance();
10019 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10020 // <table> [alias]`. PG requires an alias after a subquery source.
10021 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10022 self.advance(); // (
10023 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10024 // constant-SELECT lowering the derived-table parser uses
10025 // (PG deletes through this form; it was a parse error).
10026 let inner = if matches!(self.peek(), Token::Values) {
10027 self.advance(); // VALUES
10028 Statement::Select(self.parse_values_rows_body()?)
10029 } else {
10030 self.parse_select_stmt()?
10031 };
10032 match self.advance() {
10033 Token::RParen => {}
10034 other => {
10035 return Err(self.err(format!(
10036 "expected ')' after MERGE USING subquery, got {other:?}"
10037 )));
10038 }
10039 }
10040 let Statement::Select(sub) = inner else {
10041 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10042 };
10043 (String::new(), Some(Box::new(sub)))
10044 } else {
10045 (self.expect_ident_like()?, None)
10046 };
10047 let source_alias = match self.peek() {
10048 Token::Ident(s) | Token::QuotedIdent(s)
10049 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10050 {
10051 Some(self.expect_ident_like()?)
10052 }
10053 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10054 self.advance(); // AS
10055 Some(self.expect_ident_like()?)
10056 }
10057 _ => None,
10058 };
10059 // v7.39 (round 768, F31-D5) — optional positional column-alias
10060 // list after the source alias (`s(id, v)`).
10061 let mut source_column_aliases: Vec<String> = Vec::new();
10062 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10063 self.advance();
10064 loop {
10065 source_column_aliases.push(self.expect_ident_like()?);
10066 match self.peek() {
10067 Token::Comma => {
10068 self.advance();
10069 }
10070 Token::RParen => {
10071 self.advance();
10072 break;
10073 }
10074 other => {
10075 return Err(self.err(format!(
10076 "expected ',' or ')' in MERGE source column list, got {other:?}"
10077 )));
10078 }
10079 }
10080 }
10081 }
10082 if source_select.is_some() && source_alias.is_none() {
10083 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10084 }
10085 // ON
10086 if !matches!(self.peek(), Token::On) {
10087 return Err(self.err(format!(
10088 "expected ON after MERGE … USING source, got {:?}",
10089 self.peek()
10090 )));
10091 }
10092 self.advance();
10093 let on = self.parse_expr(0)?;
10094 // One or more WHEN clauses.
10095 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10096 loop {
10097 let is_when_kw = matches!(
10098 self.peek(),
10099 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10100 );
10101 if !is_when_kw {
10102 break;
10103 }
10104 self.advance(); // WHEN
10105 // [NOT] MATCHED
10106 let matched = if matches!(self.peek(), Token::Not) {
10107 self.advance();
10108 crate::ast::MergeMatched::NotMatched
10109 } else {
10110 crate::ast::MergeMatched::Matched
10111 };
10112 let is_matched_kw = matches!(
10113 self.peek(),
10114 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10115 );
10116 if !is_matched_kw {
10117 return Err(self.err(format!(
10118 "expected MATCHED in WHEN clause, got {:?}",
10119 self.peek()
10120 )));
10121 }
10122 self.advance();
10123 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10124 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10125 // to fire for target rows no source row matches.
10126 let mut matched = matched;
10127 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10128 self.advance();
10129 match self.peek() {
10130 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10131 self.advance();
10132 matched = crate::ast::MergeMatched::NotMatchedBySource;
10133 }
10134 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10135 self.advance();
10136 }
10137 other => {
10138 return Err(self.err(format!(
10139 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10140 )));
10141 }
10142 }
10143 }
10144 // Optional AND <expr>
10145 let condition = if matches!(self.peek(), Token::And) {
10146 self.advance();
10147 Some(self.parse_expr(0)?)
10148 } else {
10149 None
10150 };
10151 // THEN
10152 let is_then_kw = matches!(
10153 self.peek(),
10154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10155 );
10156 if !is_then_kw {
10157 return Err(self.err(format!(
10158 "expected THEN in WHEN clause, got {:?}",
10159 self.peek()
10160 )));
10161 }
10162 self.advance();
10163 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10164 let action = match self.peek().clone() {
10165 Token::Insert => {
10166 self.advance();
10167 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10168 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10169 // VALUES (…)` omits it and fills every column in declaration
10170 // order. PG accepts this; SPG used to require the list.
10171 let mut columns: Vec<String> = Vec::new();
10172 if matches!(self.peek(), Token::LParen) {
10173 self.advance();
10174 loop {
10175 columns.push(self.expect_ident_like()?);
10176 if matches!(self.peek(), Token::Comma) {
10177 self.advance();
10178 continue;
10179 }
10180 break;
10181 }
10182 if !matches!(self.peek(), Token::RParen) {
10183 return Err(self.err(format!(
10184 "expected ')' after INSERT column list, got {:?}",
10185 self.peek()
10186 )));
10187 }
10188 self.advance();
10189 }
10190 // VALUES (...)
10191 if !matches!(self.peek(), Token::Values) {
10192 return Err(self.err(format!(
10193 "expected VALUES in MERGE INSERT, got {:?}",
10194 self.peek()
10195 )));
10196 }
10197 self.advance();
10198 if !matches!(self.peek(), Token::LParen) {
10199 return Err(self.err(format!(
10200 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10201 self.peek()
10202 )));
10203 }
10204 self.advance();
10205 let mut values: Vec<crate::ast::Expr> = Vec::new();
10206 loop {
10207 values.push(self.parse_expr(0)?);
10208 if matches!(self.peek(), Token::Comma) {
10209 self.advance();
10210 continue;
10211 }
10212 break;
10213 }
10214 if !matches!(self.peek(), Token::RParen) {
10215 return Err(self.err(format!(
10216 "expected ')' after MERGE INSERT values, got {:?}",
10217 self.peek()
10218 )));
10219 }
10220 self.advance();
10221 // Empty column list = positional into every column, so the
10222 // count is checked against the table arity at execution.
10223 if !columns.is_empty() && columns.len() != values.len() {
10224 return Err(self.err(format!(
10225 "MERGE INSERT column count ({}) ≠ value count ({})",
10226 columns.len(),
10227 values.len()
10228 )));
10229 }
10230 crate::ast::MergeAction::Insert { columns, values }
10231 }
10232 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10233 self.advance();
10234 // SET
10235 let is_set_kw = matches!(
10236 self.peek(),
10237 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10238 );
10239 if !is_set_kw {
10240 return Err(self.err(format!(
10241 "expected SET after UPDATE in MERGE, got {:?}",
10242 self.peek()
10243 )));
10244 }
10245 self.advance();
10246 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10247 loop {
10248 let col = self.expect_ident_like()?;
10249 if !matches!(self.peek(), Token::Eq) {
10250 return Err(self.err(format!(
10251 "expected '=' in MERGE UPDATE assignment, got {:?}",
10252 self.peek()
10253 )));
10254 }
10255 self.advance();
10256 let expr = self.parse_expr(0)?;
10257 assignments.push((col, expr));
10258 if matches!(self.peek(), Token::Comma) {
10259 self.advance();
10260 continue;
10261 }
10262 break;
10263 }
10264 crate::ast::MergeAction::Update { assignments }
10265 }
10266 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10267 self.advance();
10268 crate::ast::MergeAction::Delete
10269 }
10270 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10271 self.advance();
10272 let is_nothing_kw = matches!(
10273 self.peek(),
10274 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10275 );
10276 if !is_nothing_kw {
10277 return Err(self.err(format!(
10278 "expected NOTHING after DO in MERGE clause, got {:?}",
10279 self.peek()
10280 )));
10281 }
10282 self.advance();
10283 crate::ast::MergeAction::DoNothing
10284 }
10285 other => {
10286 return Err(self.err(format!(
10287 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10288 )));
10289 }
10290 };
10291 // PG's grammar simply has no INSERT production under BY SOURCE
10292 // (a target row already exists there) — same syntax error.
10293 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10294 && matches!(action, crate::ast::MergeAction::Insert { .. })
10295 {
10296 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10297 }
10298 clauses.push(crate::ast::MergeWhenClause {
10299 matched,
10300 condition,
10301 action,
10302 });
10303 }
10304 if clauses.is_empty() {
10305 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10306 }
10307 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10308 // unconditional (no `AND`) WHEN of the same match kind: it could
10309 // never fire. Check per match kind in clause order.
10310 let mut seen_unconditional_matched = false;
10311 let mut seen_unconditional_not_matched = false;
10312 let mut seen_unconditional_by_source = false;
10313 for c in &clauses {
10314 let seen = match c.matched {
10315 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10316 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10317 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10318 };
10319 if *seen {
10320 return Err(self.err(String::from(
10321 "unreachable WHEN clause specified after unconditional WHEN clause",
10322 )));
10323 }
10324 if c.condition.is_none() {
10325 *seen = true;
10326 }
10327 }
10328 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10329 let returning = self.parse_optional_returning()?;
10330 Ok(Statement::Merge(crate::ast::MergeStatement {
10331 // Attached by `parse_with_cte_then_select` when the MERGE
10332 // heads a WITH clause (round 149).
10333 ctes: Vec::new(),
10334 target,
10335 target_alias,
10336 source,
10337 source_alias,
10338 source_select,
10339 source_column_aliases,
10340 on,
10341 clauses,
10342 returning,
10343 }))
10344 }
10345
10346 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10347 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10348 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10349 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10350 fn parse_optional_returning(
10351 &mut self,
10352 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10353 let is_returning_kw = matches!(
10354 self.peek(),
10355 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10356 );
10357 if !is_returning_kw {
10358 return Ok(None);
10359 }
10360 self.advance();
10361 let mut items = Vec::new();
10362 loop {
10363 items.push(self.parse_select_item()?);
10364 if matches!(self.peek(), Token::Comma) {
10365 self.advance();
10366 continue;
10367 }
10368 break;
10369 }
10370 Ok(Some(items))
10371 }
10372
10373 /// v6.0.4 — parse the tail of an ALTER statement after the
10374 /// leading `ALTER` keyword has been consumed. Only one form is
10375 /// supported in v6.0.4:
10376 ///
10377 /// ```text
10378 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10379 /// ```
10380 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10381 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10382 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10383 // exclusion) is accepted by stripping the `ONLY` keyword
10384 // before the table parse.
10385 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10386 // and the long PG-dump tail are accepted as no-ops.
10387 match self.advance() {
10388 Token::Index => {}
10389 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10390 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10391 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10392 Token::Table => {
10393 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10394 self.advance();
10395 }
10396 return self.parse_alter_table_after_keyword();
10397 }
10398 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10399 return self.parse_alter_policy_after_keyword();
10400 }
10401 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10402 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10403 self.advance();
10404 }
10405 return self.parse_alter_table_after_keyword();
10406 }
10407 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10408 // of the silent-noop tail.
10409 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10410 return self.parse_alter_sequence_after_keyword();
10411 }
10412 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10413 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10414 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10415 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10416 // NB: the match arm consumed `TYPE` via self.advance(); the
10417 // cursor is now at the type name — do NOT advance again.
10418 let type_name = self.expect_ident_like()?;
10419 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10420 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10421 if is_add_value {
10422 self.advance(); // ADD
10423 self.advance(); // VALUE
10424 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10425 // IF/EXISTS as identifiers.
10426 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10427 {
10428 let n1 = self.tokens.get(self.pos + 1);
10429 let n2 = self.tokens.get(self.pos + 2);
10430 if matches!(n1, Some(Token::Not))
10431 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10432 {
10433 self.advance();
10434 self.advance();
10435 self.advance();
10436 true
10437 } else {
10438 false
10439 }
10440 } else {
10441 false
10442 };
10443 let label = self.expect_string_literal()?;
10444 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10445 {
10446 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10447 self.advance();
10448 let anchor = self.expect_string_literal()?;
10449 Some((is_before, anchor))
10450 } else {
10451 None
10452 };
10453 return Ok(Statement::AlterTypeAddValue {
10454 type_name,
10455 label,
10456 if_not_exists,
10457 position,
10458 });
10459 }
10460 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10461 // Used to fall into the no-op tail below: accepted, silently
10462 // ignored. `RENAME TO <newtype>` keeps falling through.
10463 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10464 && matches!(
10465 self.tokens.get(self.pos + 1),
10466 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10467 )
10468 {
10469 self.advance(); // RENAME
10470 self.advance(); // VALUE
10471 let old = self.expect_string_literal()?;
10472 if matches!(self.peek(), Token::To) {
10473 self.advance();
10474 } else {
10475 self.expect_keyword_ident("to")?;
10476 }
10477 let new = self.expect_string_literal()?;
10478 return Ok(Statement::AlterTypeRenameValue {
10479 type_name,
10480 old,
10481 new,
10482 });
10483 }
10484 // Other ALTER TYPE forms — the ACTION stays a no-op
10485 // (pg_dump tail), but v7.39 (round 708) the NAME is
10486 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10487 // success for a type that does not exist.
10488 self.consume_until_statement_boundary();
10489 return Ok(Statement::ValidateOnly {
10490 kind: crate::ast::ValidateOnlyKind::TypeName,
10491 names: alloc::vec![type_name],
10492 });
10493 }
10494 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10495 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10496 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10497 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10498 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10499 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10500 // pg_dump no-op list below: every form used to report success
10501 // and change nothing, which is worse than refusing outright
10502 // (a migration dropping a constraint kept rejecting data).
10503 // NOTE: the enclosing `match self.advance()` already consumed
10504 // the DOMAIN keyword, so the name is next.
10505 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10506 return self.parse_alter_domain_after_keyword();
10507 }
10508 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10509 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10510 // used to fall into the pg_dump no-op tail below, so a DBA
10511 // setting a per-role default was told it worked and nothing
10512 // happened. Intercepted here, BEFORE that tail.
10513 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10514 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10515 // interception below exists: swallowed with the no-op tail, an
10516 // unknown parameter name was ACCEPTED where PG18 answers
10517 // `unrecognized configuration parameter`. SPG applies nothing
10518 // either way — there is no postgresql.auto.conf — but it now
10519 // says so about a name it does not know.
10520 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10521 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10522 // already consumed here. An extra advance eats the SET and
10523 // the parameter name is never seen — which is exactly the
10524 // bug a panic in this branch disproved: the branch WAS on
10525 // the path, the reading of it was wrong.
10526 let mut parameter = None;
10527 // SET <name> … | RESET <name> | RESET ALL
10528 if matches!(self.peek(), Token::Ident(k)
10529 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10530 {
10531 self.advance();
10532 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10533 && !n.eq_ignore_ascii_case("all")
10534 {
10535 self.advance();
10536 // A dotted GUC (`plpgsql.check_asserts`) is two
10537 // tokens; keep the whole name.
10538 let mut full = n;
10539 while matches!(self.peek(), Token::Dot) {
10540 self.advance();
10541 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10542 full.push('.');
10543 full.push_str(&t);
10544 }
10545 }
10546 parameter = Some(full);
10547 }
10548 }
10549 self.consume_until_statement_boundary();
10550 return Ok(Statement::AlterSystem { parameter });
10551 }
10552 Token::Ident(s) | Token::QuotedIdent(s)
10553 if matches!(
10554 s.to_ascii_lowercase().as_str(),
10555 "role" | "user" | "database"
10556 ) && self.peeks_db_role_setting() =>
10557 {
10558 let is_database = s.eq_ignore_ascii_case("database");
10559 return self.parse_db_role_setting(is_database);
10560 }
10561 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10562 // (the non-SET forms; SET/RESET took the branch above). The
10563 // attributes still no-op — recorded, and the ignored PASSWORD
10564 // is ledgered as its own follow-up — but the ROLE is validated:
10565 // any name was accepted for a role that does not exist.
10566 Token::Ident(s) | Token::QuotedIdent(s)
10567 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10568 {
10569 // NB: the enclosing `match self.advance()` already consumed
10570 // ROLE/USER — the round-695 trap, hit again in this round's
10571 // first draft (the name was eaten and WITH parsed as the
10572 // role). The cursor is at the name.
10573 let name = self.expect_ident_or_string()?;
10574 // v7.39 (round 750) — scan the attribute tail for
10575 // PASSWORD. Everything else stays a recorded no-op, but
10576 // a dropped credential rotation is a SECURITY bug:
10577 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10578 // changed nothing, so the old password kept working.
10579 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10580 // NULL` clears the credential.
10581 let mut password: Option<Option<String>> = None;
10582 loop {
10583 match self.peek() {
10584 Token::Semicolon | Token::Eof => break,
10585 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10586 self.advance();
10587 match self.advance() {
10588 Token::String(p) => password = Some(Some(p)),
10589 Token::Null => password = Some(None),
10590 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10591 password = Some(None);
10592 }
10593 other => {
10594 return Err(self.err(alloc::format!(
10595 "expected password string or NULL after PASSWORD, got {other:?}"
10596 )));
10597 }
10598 }
10599 }
10600 _ => {
10601 self.advance();
10602 }
10603 }
10604 }
10605 if name.eq_ignore_ascii_case("all") {
10606 // `ALTER ROLE ALL …` names every role; nothing to check.
10607 return Ok(Statement::Empty);
10608 }
10609 if let Some(pw) = password {
10610 return Ok(Statement::AlterRolePassword { name, password: pw });
10611 }
10612 return Ok(Statement::ValidateOnly {
10613 kind: crate::ast::ValidateOnlyKind::RoleName,
10614 names: alloc::vec![name],
10615 });
10616 }
10617 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10618 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10619 // list far enough to validate the NAME; the actions still no-op.
10620 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10621 // models none of them and their dumps are rare.)
10622 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10623 let name = self.expect_ident_or_string()?;
10624 self.consume_until_statement_boundary();
10625 return Ok(Statement::ValidateOnly {
10626 kind: crate::ast::ValidateOnlyKind::CollationName,
10627 names: alloc::vec![name],
10628 });
10629 }
10630 Token::Ident(s) | Token::QuotedIdent(s)
10631 if s.eq_ignore_ascii_case("text")
10632 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10633 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10634 {
10635 self.advance(); // SEARCH
10636 self.advance(); // CONFIGURATION
10637 let name = self.expect_ident_like()?;
10638 self.consume_until_statement_boundary();
10639 return Ok(Statement::ValidateOnly {
10640 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10641 names: alloc::vec![name],
10642 });
10643 }
10644 Token::Ident(s) | Token::QuotedIdent(s)
10645 if s.eq_ignore_ascii_case("event")
10646 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10647 {
10648 self.advance(); // TRIGGER
10649 let name = self.expect_ident_like()?;
10650 self.consume_until_statement_boundary();
10651 return Ok(Statement::ValidateOnly {
10652 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10653 names: alloc::vec![name],
10654 });
10655 }
10656 Token::Ident(s) | Token::QuotedIdent(s)
10657 if s.eq_ignore_ascii_case("large")
10658 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10659 {
10660 self.advance(); // OBJECT
10661 let oid = match self.advance() {
10662 Token::Integer(n) => alloc::format!("{n}"),
10663 other => {
10664 return Err(
10665 self.err(alloc::format!("expected large object oid, got {other:?}"))
10666 );
10667 }
10668 };
10669 self.consume_until_statement_boundary();
10670 return Ok(Statement::ValidateOnly {
10671 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10672 names: alloc::vec![oid],
10673 });
10674 }
10675 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10676 // argument-list parse as DROP AGGREGATE (round 707); the
10677 // action no-ops, the existence check is real.
10678 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10679 // Same round-695 trap as above: AGGREGATE is already
10680 // consumed; the cursor is at the name.
10681 let name = self.expect_ident_like()?;
10682 let mut names = alloc::vec![name];
10683 if matches!(self.peek(), Token::LParen) {
10684 self.advance();
10685 loop {
10686 match self.peek().clone() {
10687 Token::RParen => {
10688 self.advance();
10689 break;
10690 }
10691 Token::Star => {
10692 self.advance();
10693 names.push(String::from("*"));
10694 }
10695 Token::Comma => {
10696 self.advance();
10697 }
10698 _ => {
10699 let mut t = self.expect_ident_like()?;
10700 while let Token::Ident(nx) = self.peek() {
10701 let nx = nx.clone();
10702 self.advance();
10703 t.push(' ');
10704 t.push_str(&nx);
10705 }
10706 names.push(t);
10707 }
10708 }
10709 }
10710 }
10711 self.consume_until_statement_boundary();
10712 return Ok(Statement::ValidateOnly {
10713 kind: crate::ast::ValidateOnlyKind::AggregateName,
10714 names,
10715 });
10716 }
10717 Token::Ident(s) | Token::QuotedIdent(s)
10718 if matches!(
10719 s.to_ascii_lowercase().as_str(),
10720 "view"
10721 | "function"
10722 | "database"
10723 | "schema"
10724 | "owner"
10725 | "default"
10726 | "extension"
10727 | "materialized"
10728 | "publication"
10729 | "subscription"
10730 // v7.37.17 (17.6 siblings) — additional ALTER
10731 // targets pg_dump / pg_dumpall / operator DB
10732 // migration scripts commonly emit. SPG has
10733 // no matching machinery for any of these; the
10734 // parser accepts + Empty-returns so pg_dump
10735 // tail statements don't stall.
10736 | "tablespace"
10737 | "language"
10738 | "operator"
10739 | "conversion"
10740 | "statistics"
10741 | "server"
10742 | "foreign"
10743 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10744 // / TEMPLATE (CONFIGURATION intercepted above).
10745 | "text"
10746 ) =>
10747 {
10748 self.consume_until_statement_boundary();
10749 return Ok(Statement::Empty);
10750 }
10751 other => {
10752 return Err(self.err(format!(
10753 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10754 after ALTER, got {other:?}"
10755 )));
10756 }
10757 }
10758 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10759 // (mailrs migrate-042 ships these). The presence of an
10760 // IF EXISTS makes the subsequent name lookup tolerate
10761 // a missing index — engine returns CommandOk no-op.
10762 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10763 let next = self.tokens.get(self.pos + 1);
10764 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10765 self.advance();
10766 self.advance();
10767 true
10768 } else {
10769 false
10770 }
10771 } else {
10772 false
10773 };
10774 let name = self.expect_ident_like()?;
10775 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10776 // Detect BEFORE the REBUILD path so the existing REBUILD
10777 // arm stays untouched.
10778 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10779 self.advance();
10780 if matches!(self.peek(), Token::To) {
10781 self.advance();
10782 } else {
10783 self.expect_keyword_ident("to")?;
10784 }
10785 let new = self.expect_ident_like()?;
10786 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10787 name,
10788 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10789 }));
10790 }
10791 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10792 // A syntax error before; the index is validated, the params no-op.
10793 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10794 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10795 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10796 {
10797 self.consume_until_statement_boundary();
10798 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10799 name,
10800 target: crate::ast::AlterIndexTarget::StorageParams,
10801 }));
10802 }
10803 // REBUILD
10804 self.expect_keyword_ident("rebuild")?;
10805 // Optional: WITH (encoding = <enc>)
10806 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10807 self.advance();
10808 if !matches!(self.peek(), Token::LParen) {
10809 return Err(self.err(format!(
10810 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10811 self.peek()
10812 )));
10813 }
10814 self.advance();
10815 self.expect_keyword_ident("encoding")?;
10816 if !matches!(self.peek(), Token::Eq) {
10817 return Err(self.err(format!(
10818 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10819 self.peek()
10820 )));
10821 }
10822 self.advance();
10823 let enc_ident = match self.advance() {
10824 Token::Ident(s) | Token::QuotedIdent(s) => s,
10825 other => {
10826 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10827 }
10828 };
10829 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10830 "f32" => VecEncoding::F32,
10831 "sq8" => VecEncoding::Sq8,
10832 "half" => VecEncoding::F16,
10833 other => {
10834 return Err(self.err(format!(
10835 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10836 )));
10837 }
10838 };
10839 if !matches!(self.peek(), Token::RParen) {
10840 return Err(self.err(format!(
10841 "expected ')' after encoding value, got {:?}",
10842 self.peek()
10843 )));
10844 }
10845 self.advance();
10846 Some(enc)
10847 } else {
10848 None
10849 };
10850 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10851 name,
10852 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10853 }))
10854 }
10855
10856 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10857 /// only `SET` form currently supported; future v6.7.x can add
10858 /// more SET subjects without changing the dispatch shape.
10859 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10860 /// subactions. Single-subaction shape stays a 1-element vec.
10861 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10862 let table_name = self.expect_ident_like()?;
10863 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10864 loop {
10865 let subaction = self.parse_alter_table_subaction()?;
10866 // ADD COLUMN with inline REFERENCES emits both an
10867 // AddColumn and an AddForeignKey subaction; the
10868 // helper returns 1 or 2 items.
10869 targets.extend(subaction);
10870 if matches!(self.peek(), Token::Comma) {
10871 self.advance();
10872 continue;
10873 }
10874 break;
10875 }
10876 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10877 name: table_name,
10878 targets,
10879 }))
10880 }
10881
10882 /// Parse one ALTER TABLE subaction. Returns a Vec because
10883 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10884 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10885 fn parse_alter_table_subaction(
10886 &mut self,
10887 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10888 match self.peek() {
10889 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10890 self.advance();
10891 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10892 // storage parameters: paren-prefixed; consume.
10893 if matches!(self.peek(), Token::LParen) {
10894 self.consume_until_statement_boundary();
10895 return Ok(Vec::new());
10896 }
10897 let setting = self.expect_ident_like()?;
10898 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10899 if !matches!(self.peek(), Token::Eq) {
10900 return Err(self.err(alloc::format!(
10901 "expected '=' after hot_tier_bytes, got {:?}",
10902 self.peek()
10903 )));
10904 }
10905 self.advance();
10906 let n = self.expect_u64_literal()?;
10907 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10908 }
10909 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10910 // accept-and-no-op for ALTER TABLE SET <subject>
10911 // forms that pg_dump emits but SPG either treats
10912 // as N/A (single-tenant, single-owner, no shared
10913 // tablespaces) or accepts the dump-side declaration
10914 // without runtime effect:
10915 // SET SCHEMA <name> (18.11)
10916 // SET TABLESPACE <name> (18.8)
10917 // SET LOGGED / UNLOGGED (18.7 alt-form)
10918 // SET WITHOUT CLUSTER (18.13)
10919 // SET WITHOUT OIDS (PG legacy)
10920 // SET (option = value, …) (storage parameters)
10921 // SET REPLICA IDENTITY {…} (18.14)
10922 if setting.eq_ignore_ascii_case("schema")
10923 || setting.eq_ignore_ascii_case("tablespace")
10924 || setting.eq_ignore_ascii_case("logged")
10925 || setting.eq_ignore_ascii_case("unlogged")
10926 || setting.eq_ignore_ascii_case("without")
10927 {
10928 self.consume_until_statement_boundary();
10929 return Ok(Vec::new());
10930 }
10931 if setting.eq_ignore_ascii_case("replica") {
10932 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10933 self.consume_until_statement_boundary();
10934 return Ok(Vec::new());
10935 }
10936 // SET (option=value, …) — storage parameters.
10937 if matches!(self.peek(), Token::LParen) {
10938 self.consume_until_statement_boundary();
10939 return Ok(Vec::new());
10940 }
10941 Err(self.err(alloc::format!(
10942 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10943 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10944 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10945 )))
10946 }
10947 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10948 // not ignored: round 645 gave SPG the inheritance the
10949 // v7.37.18 no-op said it did not have.
10950 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10951 self.advance();
10952 let parent = self.expect_ident_like()?;
10953 self.consume_until_statement_boundary();
10954 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10955 parent,
10956 detach: false
10957 }])
10958 }
10959 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10960 // LEVEL SECURITY`, which has its own RLS arm below — without
10961 // the guard this swallowed NO FORCE as a no-op.
10962 Token::Ident(s)
10963 if s.eq_ignore_ascii_case("no")
10964 && !matches!(
10965 self.tokens.get(self.pos + 1),
10966 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10967 ) =>
10968 {
10969 self.advance();
10970 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10971 if k.eq_ignore_ascii_case("inherit"))
10972 {
10973 self.advance();
10974 let parent = self.expect_ident_like()?;
10975 self.consume_until_statement_boundary();
10976 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10977 parent,
10978 detach: true
10979 }]);
10980 }
10981 self.consume_until_statement_boundary();
10982 Ok(Vec::new())
10983 }
10984 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10985 // single-owner, so there is still nothing to record.
10986 //
10987 // v7.39 (round 652) — but the name now reaches the engine,
10988 // which refuses a role that does not exist as PG does. The
10989 // no-op was swallowing the whole statement, so a dump naming
10990 // a role this server never heard of restored clean and left
10991 // the table owned by whoever ran the restore.
10992 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10993 self.advance();
10994 if matches!(self.peek(), Token::To) {
10995 self.advance();
10996 }
10997 let role = self.expect_ident_like()?;
10998 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10999 role
11000 }])
11001 }
11002 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11003 // PG sets a hint; SPG doesn't have clustered storage, so the
11004 // hint itself stays a no-op.
11005 //
11006 // v7.39 (round 652) — the index name is checked now. PG
11007 // errors on one that does not exist, and swallowing that let
11008 // a typo'd CLUSTER ON pass silently.
11009 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11010 self.advance();
11011 // `ON` is a reserved token, not an ident.
11012 if !matches!(self.peek(), Token::On) {
11013 return Err(self.err(alloc::format!(
11014 "expected ON after CLUSTER, got {:?}",
11015 self.peek()
11016 )));
11017 }
11018 self.advance();
11019 let index = self.expect_ident_like()?;
11020 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11021 index: Some(index)
11022 }])
11023 }
11024 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11025 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11026 // what a logical decoder puts in the old-tuple image; SPG's
11027 // replication is SQL-text, so there is nothing to record.
11028 // Accept-and-no-op (it used to be a parse error).
11029 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11030 self.advance();
11031 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11032 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11033 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11034 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11035 {
11036 self.advance(); // IDENTITY
11037 self.advance(); // USING
11038 if matches!(self.peek(), Token::Index)
11039 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11040 {
11041 self.advance();
11042 }
11043 let index = self.expect_ident_like()?;
11044 self.consume_until_statement_boundary();
11045 return Ok(alloc::vec![
11046 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11047 ]);
11048 }
11049 self.consume_until_statement_boundary();
11050 Ok(Vec::new())
11051 }
11052 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11053 //
11054 // v7.39 (round 652) — it used to consume the statement and
11055 // return nothing, on the stated theory that SPG validated at
11056 // ADD CONSTRAINT time so there was never anything left to
11057 // validate. Measured against PG18, ADD CONSTRAINT did not
11058 // scan the existing rows at all — the comment described a
11059 // property SPG did not have, which is why nobody looked. Both
11060 // halves are real now: ADD scans unless told NOT VALID, and
11061 // this scans what NOT VALID skipped.
11062 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11063 self.advance();
11064 self.expect_keyword_ident("constraint")?;
11065 let name = self.expect_ident_like()?;
11066 Ok(alloc::vec![
11067 crate::ast::AlterTableTarget::ValidateConstraint { name }
11068 ])
11069 }
11070 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11071 // SET (option = value, …). PG uses it to clear per-table
11072 // storage params like fillfactor or autovacuum_*. SPG
11073 // engine-manages those parameters; accept-and-no-op.
11074 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11075 self.advance();
11076 self.consume_until_statement_boundary();
11077 Ok(Vec::new())
11078 }
11079 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11080 // type-of binding (PG 9.0+). SPG composite types
11081 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11082 // TABLE OF is rare and inverse of CREATE TABLE OF.
11083 // Accept-and-no-op until a customer dump round-trips it.
11084 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11085 self.advance();
11086 // v7.39 (round 710) — the type name is validated now.
11087 let type_name = self.expect_ident_like()?;
11088 self.consume_until_statement_boundary();
11089 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11090 type_name
11091 }])
11092 }
11093 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11094 // (reserved keyword) rather than Token::Ident("not"),
11095 // so it needs its own arm. Accept-and-no-op same as OF.
11096 Token::Not => {
11097 self.advance();
11098 self.consume_until_statement_boundary();
11099 Ok(Vec::new())
11100 }
11101 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11102 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11103 self.advance();
11104 self.expect_row_level_security()?;
11105 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11106 enabled: None,
11107 force: Some(true),
11108 }])
11109 }
11110 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11111 Token::Ident(s)
11112 if s.eq_ignore_ascii_case("no")
11113 && matches!(
11114 self.tokens.get(self.pos + 1),
11115 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11116 ) =>
11117 {
11118 self.advance(); // NO
11119 self.advance(); // FORCE
11120 self.expect_row_level_security()?;
11121 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11122 enabled: None,
11123 force: Some(false),
11124 }])
11125 }
11126 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11127 // (sets relrowsecurity). The guard requires the next token to be
11128 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11129 Token::Ident(s)
11130 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11131 && matches!(
11132 self.tokens.get(self.pos + 1),
11133 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11134 ) =>
11135 {
11136 let enabled = s.eq_ignore_ascii_case("enable");
11137 self.advance(); // ENABLE/DISABLE
11138 self.expect_row_level_security()?;
11139 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11140 enabled: Some(enabled),
11141 force: None,
11142 }])
11143 }
11144 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11145 self.advance();
11146 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11147 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11148 // emits. The same grammar CREATE TABLE already accepts
11149 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11150 // through the SAME parser — an ALTER-only copy would be a
11151 // second place for the two to drift.
11152 if self.peek_mysql_inline_key_start() {
11153 return Ok(match self.parse_mysql_inline_key()? {
11154 Some(c) => {
11155 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11156 }
11157 // FULLTEXT / SPATIAL parse and are accepted as a
11158 // no-op here exactly as they are inline.
11159 None => Vec::new(),
11160 });
11161 }
11162 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11163 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11164 // PRIMARY KEY this way; mysqldump emits both.
11165 // Peek-only dispatch (no advance) — `advance()`
11166 // destructively replaces consumed tokens with Eof,
11167 // so saved-pos restore would land on Eofs.
11168 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11169 {
11170 // The next-but-one ident is the constraint
11171 // name; the one after THAT is the kind.
11172 let kind_pos = self.pos + 2;
11173 let kind = self.tokens.get(kind_pos).cloned();
11174 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11175 {
11176 let fk = self.parse_table_level_fk()?;
11177 return Ok(alloc::vec![
11178 crate::ast::AlterTableTarget::AddForeignKey(fk)
11179 ]);
11180 }
11181 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11182 {
11183 self.advance(); // CONSTRAINT
11184 // v7.39 (read01 round 48) — keep the name; the engine
11185 // stores it now instead of dropping it on the floor.
11186 let con_name = self.expect_ident_like()?;
11187 self.advance(); // PRIMARY
11188 self.expect_keyword_ident("key")?;
11189 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11190 // v7.39 (round 711) — the ALTER form carries the
11191 // timing too (pg_dump writes it here).
11192 let (deferrable, initially_deferred) =
11193 self.consume_deferrable_clauses_timed()?;
11194 return Ok(alloc::vec![
11195 crate::ast::AlterTableTarget::AddTableConstraint(
11196 crate::ast::TableConstraint::PrimaryKey {
11197 name: Some(con_name),
11198 columns: cols,
11199 deferrable,
11200 initially_deferred,
11201 }
11202 )
11203 ]);
11204 }
11205 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11206 {
11207 self.advance(); // CONSTRAINT
11208 // v7.39 (read01 round 48) — keep the name.
11209 let con_name = self.expect_ident_like()?;
11210 // v7.22 (mailrs round-13 gap 6) — delegate so
11211 // the optional `NULLS [NOT] DISTINCT` modifier
11212 // parses here too (pg_dump emits the ALTER
11213 // form; semantics enforced by the engine
11214 // since v7.13).
11215 let mut uc = self.parse_table_level_unique()?;
11216 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11217 *name = Some(con_name);
11218 }
11219 return Ok(alloc::vec![
11220 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11221 ]);
11222 }
11223 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11224 {
11225 self.advance(); // CONSTRAINT
11226 // v7.39 (read01 round 48) — keep the name.
11227 let con_name = self.expect_ident_like()?;
11228 self.advance(); // CHECK
11229 if !matches!(self.peek(), Token::LParen) {
11230 return Err(self.err(alloc::format!(
11231 "expected '(' after CHECK, got {:?}", self.peek()
11232 )));
11233 }
11234 self.advance();
11235 let expr = self.parse_expr(0)?;
11236 if matches!(self.peek(), Token::RParen) {
11237 self.advance();
11238 }
11239 let not_valid = self.parse_not_valid_suffix();
11240 return Ok(alloc::vec![
11241 crate::ast::AlterTableTarget::AddTableConstraint(
11242 crate::ast::TableConstraint::Check {
11243 name: Some(con_name),
11244 expr,
11245 not_valid,
11246 }
11247 )
11248 ]);
11249 }
11250 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11251 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11252 // exclusion constraints via this ALTER form.
11253 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11254 {
11255 self.advance(); // CONSTRAINT
11256 let con_name = self.expect_ident_like()?;
11257 let mut ex = self.parse_table_level_exclude()?;
11258 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11259 *name = Some(con_name);
11260 }
11261 return Ok(alloc::vec![
11262 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11263 ]);
11264 }
11265 // Unknown kind — fall through to FK path which
11266 // produces a descriptive parse error.
11267 }
11268 let is_fk = matches!(
11269 self.peek(),
11270 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11271 || s.eq_ignore_ascii_case("foreign")
11272 );
11273 if is_fk {
11274 let fk = self.parse_table_level_fk()?;
11275 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11276 }
11277 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11278 // (no CONSTRAINT prefix) — same dispatch.
11279 match self.peek().clone() {
11280 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11281 self.advance();
11282 self.expect_keyword_ident("key")?;
11283 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11284 let (deferrable, initially_deferred) =
11285 self.consume_deferrable_clauses_timed()?;
11286 return Ok(alloc::vec![
11287 crate::ast::AlterTableTarget::AddTableConstraint(
11288 crate::ast::TableConstraint::PrimaryKey {
11289 name: None,
11290 columns: cols,
11291 deferrable,
11292 initially_deferred,
11293 }
11294 )
11295 ]);
11296 }
11297 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11298 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11299 let uc = self.parse_table_level_unique()?;
11300 return Ok(alloc::vec![
11301 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11302 ]);
11303 }
11304 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11305 // prefix). The other three bare forms were here and
11306 // this one was not, so it fell through to the column
11307 // path and came back as "unexpected reserved keyword
11308 // 'check' at start of column definition".
11309 _ if self.peek_table_level_check_start() => {
11310 let chk = self.parse_table_level_check()?;
11311 let not_valid = self.parse_not_valid_suffix();
11312 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11313 unreachable!("parse_table_level_check returns Check")
11314 };
11315 return Ok(alloc::vec![
11316 crate::ast::AlterTableTarget::AddTableConstraint(
11317 crate::ast::TableConstraint::Check {
11318 name: None,
11319 expr,
11320 not_valid,
11321 }
11322 )
11323 ]);
11324 }
11325 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11326 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11327 let ex = self.parse_table_level_exclude()?;
11328 return Ok(alloc::vec![
11329 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11330 ]);
11331 }
11332 _ => {}
11333 }
11334 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11335 self.advance();
11336 }
11337 let mut if_not_exists = false;
11338 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11339 self.advance();
11340 if !matches!(self.peek(), Token::Not) {
11341 return Err(self.err(alloc::format!(
11342 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11343 self.peek()
11344 )));
11345 }
11346 self.advance();
11347 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11348 return Err(self.err(alloc::format!(
11349 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11350 self.peek()
11351 )));
11352 }
11353 self.advance();
11354 if_not_exists = true;
11355 }
11356 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11357 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11358 // returns ColumnDef + an optional inline FK.
11359 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11360 let col_name = column.name.clone();
11361 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11362 column,
11363 if_not_exists,
11364 }];
11365 if let Some(mut fk) = col_level_fk {
11366 if fk.columns.is_empty() {
11367 fk.columns.push(col_name);
11368 }
11369 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11370 }
11371 Ok(out)
11372 }
11373 Token::Drop => {
11374 self.advance();
11375 // v7.13.3 — dispatch on the next token. mailrs round-7
11376 // S8 closed DROP COLUMN; round-6 S7 closed
11377 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11378 // RESTRICT modifiers.
11379 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11380 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11381 let subject = match self.peek() {
11382 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11383 self.advance();
11384 "constraint"
11385 }
11386 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11387 self.advance();
11388 "column"
11389 }
11390 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11391 // `INDEX` lexes as the reserved Token::Index, so it is
11392 // unambiguous. `KEY` is a plain ident, and PG allows a
11393 // column literally named "key", so only read it as the
11394 // keyword when a name follows it.
11395 Token::Index => {
11396 self.advance();
11397 "index"
11398 }
11399 Token::Ident(s)
11400 if s.eq_ignore_ascii_case("key")
11401 && matches!(
11402 self.tokens.get(self.pos + 1),
11403 Some(Token::Ident(_) | Token::QuotedIdent(_))
11404 ) =>
11405 {
11406 self.advance();
11407 "index"
11408 }
11409 // PG-canonical bare `DROP <col>` without COLUMN
11410 // keyword is also valid; treat any other ident
11411 // as the column name.
11412 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11413 other => {
11414 return Err(self.err(alloc::format!(
11415 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11416 )));
11417 }
11418 };
11419 let mut if_exists = false;
11420 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11421 let n1 = self.tokens.get(self.pos + 1);
11422 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11423 self.advance();
11424 self.advance();
11425 if_exists = true;
11426 }
11427 }
11428 let name = self.expect_ident_like()?;
11429 let mut cascade = false;
11430 if matches!(
11431 self.peek(),
11432 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11433 || s.eq_ignore_ascii_case("restrict")
11434 ) {
11435 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11436 {
11437 cascade = true;
11438 }
11439 self.advance();
11440 }
11441 if subject == "index" {
11442 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11443 name,
11444 if_exists,
11445 }])
11446 } else if subject == "constraint" {
11447 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11448 name,
11449 if_exists,
11450 }])
11451 } else {
11452 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11453 column: name,
11454 if_exists,
11455 cascade,
11456 }])
11457 }
11458 }
11459 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11460 self.advance();
11461 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11462 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11463 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11464 // immediately; accept-and-no-op.
11465 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11466 self.advance();
11467 self.consume_until_statement_boundary();
11468 return Ok(Vec::new());
11469 }
11470 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11471 self.advance();
11472 }
11473 let col_name = self.expect_ident_like()?;
11474 match self.peek() {
11475 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11476 self.advance();
11477 }
11478 // v7.14.0 — pg_dump emits BIGSERIAL via
11479 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11480 // nextval('seq')` (the sequence is created
11481 // separately). SPG's BIGSERIAL already uses
11482 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11483 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11484 // engine no-ops by consuming the tail.
11485 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11486 // v7.22 (round-13 T2) — `SET DEFAULT
11487 // nextval('…')` is how pg_dump spells a
11488 // SERIAL column (plain integer in CREATE
11489 // TABLE + this ALTER). It used to be
11490 // swallowed as a no-op, which silently
11491 // STRIPPED auto-increment from imported
11492 // schemas — the first post-import INSERT
11493 // without an explicit id then violated NOT
11494 // NULL. Lower it to the auto-increment
11495 // marker instead.
11496 let is_default_nextval =
11497 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11498 && matches!(
11499 self.tokens.get(self.pos + 2),
11500 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11501 );
11502 if is_default_nextval {
11503 let seq_name = self.scan_sequence_name_until_boundary();
11504 return Ok(alloc::vec![
11505 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11506 column: col_name,
11507 seq_name,
11508 }
11509 ]);
11510 }
11511 // v7.37.18 (18.1 + 18.2) — proper lowering.
11512 self.advance(); // consume "set"
11513 match self.peek().clone() {
11514 Token::Default => {
11515 self.advance();
11516 let default_expr = self.parse_expr(0)?;
11517 return Ok(alloc::vec![
11518 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11519 column: col_name,
11520 default_expr,
11521 }
11522 ]);
11523 }
11524 Token::Not => {
11525 self.advance();
11526 if !matches!(self.peek(), Token::Null) {
11527 return Err(self.err(alloc::format!(
11528 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11529 self.peek()
11530 )));
11531 }
11532 self.advance();
11533 return Ok(alloc::vec![
11534 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11535 column: col_name,
11536 }
11537 ]);
11538 }
11539 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11540 // stored generated column's expression and
11541 // recompute existing rows.
11542 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11543 self.advance(); // EXPRESSION
11544 if matches!(self.peek(), Token::As) {
11545 self.advance();
11546 }
11547 let expr = self.parse_expr(0)?;
11548 return Ok(alloc::vec![
11549 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11550 column: col_name,
11551 expr,
11552 }
11553 ]);
11554 }
11555 other => {
11556 // Other SET subjects (STATISTICS,
11557 // STORAGE, COMPRESSION, …) stay no-ops —
11558 // storage hints with no SPG semantics.
11559 let _ = other;
11560 self.consume_until_statement_boundary();
11561 return Ok(Vec::new());
11562 }
11563 }
11564 }
11565 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11566 self.advance(); // consume "drop"
11567 return self.parse_alter_column_drop_tail(col_name);
11568 }
11569 Token::Drop => {
11570 self.advance(); // consume Drop token
11571 return self.parse_alter_column_drop_tail(col_name);
11572 }
11573 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11574 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11575 // GENERATED { ALWAYS | BY DEFAULT } AS
11576 // IDENTITY ( … )`: pg_dump's spelling for
11577 // identity columns. Same auto-increment
11578 // lowering as the nextval default; the
11579 // sequence options inside the parens are
11580 // no-ops under SPG's max+1 semantics.
11581 let is_generated = matches!(
11582 self.tokens.get(self.pos + 1),
11583 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11584 );
11585 if !is_generated {
11586 return Err(self.err(alloc::format!(
11587 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11588 self.tokens.get(self.pos + 1)
11589 )));
11590 }
11591 let seq_name = self.scan_sequence_name_until_boundary();
11592 return Ok(alloc::vec![
11593 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11594 column: col_name,
11595 seq_name,
11596 }
11597 ]);
11598 }
11599 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11600 // column: floor the next allocated value at n (bare
11601 // RESTART = restart from the start value, 1).
11602 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11603 self.advance();
11604 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11605 {
11606 self.advance();
11607 let neg = if matches!(self.peek(), Token::Minus) {
11608 self.advance();
11609 true
11610 } else {
11611 false
11612 };
11613 match self.advance() {
11614 Token::Integer(v) => Some(if neg { -v } else { v }),
11615 other => {
11616 return Err(self.err(alloc::format!(
11617 "expected integer after RESTART WITH, got {other:?}"
11618 )));
11619 }
11620 }
11621 } else {
11622 None
11623 };
11624 return Ok(alloc::vec![
11625 crate::ast::AlterTableTarget::AlterColumnRestart {
11626 column: col_name,
11627 with,
11628 }
11629 ]);
11630 }
11631 other => {
11632 return Err(self.err(alloc::format!(
11633 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11634 )));
11635 }
11636 }
11637 // v7.39 (round 713) — the type parser has consumed a
11638 // trailing `COLLATE <name>` since Phase 2.5, and
11639 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11640 // TYPE text COLLATE "C"` parsed clean and changed
11641 // nothing. Keep the clause; the engine re-collates.
11642 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11643 self.parse_type_with_implied_flags()?;
11644 let collation = if coll_explicit {
11645 coll_name.map(|n| (coll, n))
11646 } else {
11647 None
11648 };
11649 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11650 {
11651 self.advance();
11652 Some(self.parse_expr(0)?)
11653 } else {
11654 None
11655 };
11656 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11657 column: col_name,
11658 new_type,
11659 using,
11660 collation,
11661 }])
11662 }
11663 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11664 // PG also supports `RENAME TO new_table` for table-name
11665 // rename; that surface is deferred (pg_dump never emits
11666 // it). If the first post-RENAME ident is `TO`, the user
11667 // is asking for table rename — error with a clear
11668 // message rather than misparsing `TO` as a column name.
11669 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11670 self.advance();
11671 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11672 // table-name rename (mailrs round-10 A.5 — used
11673 // by migrate-042's `RENAME TO email_contacts`).
11674 // `TO` lexes as Token::To.
11675 if matches!(self.peek(), Token::To)
11676 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11677 {
11678 self.advance();
11679 let new = self.expect_ident_like()?;
11680 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11681 new,
11682 }]);
11683 }
11684 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11685 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11686 self.advance();
11687 let old = self.expect_ident_like()?;
11688 if matches!(self.peek(), Token::To) {
11689 self.advance();
11690 } else {
11691 self.expect_keyword_ident("to")?;
11692 }
11693 let new = self.expect_ident_like()?;
11694 return Ok(alloc::vec![
11695 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11696 ]);
11697 }
11698 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11699 self.advance();
11700 }
11701 let old = self.expect_ident_like()?;
11702 // `TO` is a reserved keyword token; accept both
11703 // Token::To and Token::Ident("to") for consistency.
11704 if matches!(self.peek(), Token::To) {
11705 self.advance();
11706 } else {
11707 self.expect_keyword_ident("to")?;
11708 }
11709 let new = self.expect_ident_like()?;
11710 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11711 old,
11712 new,
11713 }])
11714 }
11715 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11716 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11717 // every data block with these. Real disable semantics —
11718 // not no-op — because reload correctness assumes the
11719 // triggers don't fire (rows already carry their
11720 // computed values from prod).
11721 Token::Ident(s)
11722 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11723 {
11724 let enabled = s.eq_ignore_ascii_case("enable");
11725 self.advance();
11726 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11727 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11728 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11729 // pg_dump output) — anything else falls through to
11730 // the catch-all error below.
11731 // v7.22 (round-13 T3) — mysqldump wraps every data
11732 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11733 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11734 // maintains indexes incrementally — engine no-op.
11735 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11736 self.advance();
11737 return Ok(Vec::new());
11738 }
11739 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11740 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11741 // to gate triggers on session_replication_role; SPG
11742 // has no replica role, so the prefix is consumed and
11743 // treated identically to the plain ENABLE/DISABLE
11744 // TRIGGER form.
11745 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11746 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11747 {
11748 self.advance();
11749 }
11750 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11751 return Err(self.err(alloc::format!(
11752 "expected TRIGGER after {}, got {:?}",
11753 if enabled { "ENABLE" } else { "DISABLE" },
11754 self.peek()
11755 )));
11756 }
11757 self.advance();
11758 // `ALL` lexes as Token::All (reserved); also
11759 // accept Token::Ident("all") for symmetry.
11760 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11761 // TRIGGER selectors. USER (= all user triggers) is
11762 // semantically ALL here; REPLICA / ALWAYS gate on
11763 // session_replication_role which SPG doesn't track.
11764 // All map to TriggerSelector::All.
11765 let which = if matches!(self.peek(), Token::All)
11766 || matches!(self.peek(), Token::Ident(s)
11767 if s.eq_ignore_ascii_case("all")
11768 || s.eq_ignore_ascii_case("user")
11769 || s.eq_ignore_ascii_case("replica")
11770 || s.eq_ignore_ascii_case("always"))
11771 {
11772 self.advance();
11773 crate::ast::TriggerSelector::All
11774 } else {
11775 let name = self.expect_ident_like()?;
11776 crate::ast::TriggerSelector::Named(name)
11777 };
11778 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11779 which,
11780 enabled,
11781 }])
11782 }
11783 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11784 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11785 self.advance();
11786 if !matches!(self.peek(), Token::Partition)
11787 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11788 if s.eq_ignore_ascii_case("partition"))
11789 {
11790 return Err(self.err(alloc::format!(
11791 "expected PARTITION after ATTACH, got {:?}",
11792 self.peek()
11793 )));
11794 }
11795 self.advance();
11796 let child = self.expect_ident_like()?;
11797 let bounds = self.parse_partition_bounds_tail()?;
11798 Ok(alloc::vec![
11799 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11800 ])
11801 }
11802 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11803 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11804 self.advance();
11805 if !matches!(self.peek(), Token::Partition)
11806 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11807 if s.eq_ignore_ascii_case("partition"))
11808 {
11809 return Err(self.err(alloc::format!(
11810 "expected PARTITION after DETACH, got {:?}",
11811 self.peek()
11812 )));
11813 }
11814 self.advance();
11815 let child = self.expect_ident_like()?;
11816 let mut concurrently = false;
11817 let mut finalize = false;
11818 loop {
11819 match self.peek().clone() {
11820 Token::Ident(s) | Token::QuotedIdent(s)
11821 if s.eq_ignore_ascii_case("concurrently") =>
11822 {
11823 self.advance();
11824 concurrently = true;
11825 }
11826 Token::Ident(s) | Token::QuotedIdent(s)
11827 if s.eq_ignore_ascii_case("finalize") =>
11828 {
11829 self.advance();
11830 finalize = true;
11831 }
11832 _ => break,
11833 }
11834 }
11835 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11836 child,
11837 concurrently,
11838 finalize,
11839 }])
11840 }
11841 other => Err(self.err(alloc::format!(
11842 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11843 ))),
11844 }
11845 }
11846
11847 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11848 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11849 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11850 /// `parse_partition_of_tail`'s bounds branch.
11851 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11852 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11853 /// lowering each to the respective AlterTableTarget. Any
11854 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11855 /// no-op via consume_until_statement_boundary.
11856 fn parse_alter_column_drop_tail(
11857 &mut self,
11858 col_name: String,
11859 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11860 match self.peek().clone() {
11861 Token::Default => {
11862 self.advance();
11863 Ok(alloc::vec![
11864 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11865 ])
11866 }
11867 Token::Not => {
11868 self.advance();
11869 if !matches!(self.peek(), Token::Null) {
11870 return Err(self.err(alloc::format!(
11871 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11872 self.peek()
11873 )));
11874 }
11875 self.advance();
11876 Ok(alloc::vec![
11877 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11878 ])
11879 }
11880 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11881 // generated column into a plain column.
11882 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11883 self.advance();
11884 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11885 // dropped, so the engine still errored on a plain
11886 // column; PG's semantics are NOTICE + skip.
11887 let mut if_exists = false;
11888 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11889 self.advance();
11890 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11891 self.advance();
11892 if_exists = true;
11893 }
11894 }
11895 Ok(alloc::vec![
11896 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11897 column: col_name,
11898 if_exists,
11899 }
11900 ])
11901 }
11902 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11903 // identity column into a plain column.
11904 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11905 self.advance();
11906 let mut if_exists = false;
11907 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11908 self.advance();
11909 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11910 self.advance();
11911 if_exists = true;
11912 }
11913 }
11914 Ok(alloc::vec![
11915 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11916 column: col_name,
11917 if_exists,
11918 }
11919 ])
11920 }
11921 _ => {
11922 self.consume_until_statement_boundary();
11923 Ok(Vec::new())
11924 }
11925 }
11926 }
11927
11928 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11929 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11930 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11931 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11932 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11933 let mut opts = crate::ast::CopyOptions::default();
11934 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11935 return Ok(opts);
11936 }
11937 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11938 self.advance();
11939 }
11940 if matches!(self.peek(), Token::LParen) {
11941 self.advance();
11942 loop {
11943 self.parse_one_copy_option(&mut opts)?;
11944 match self.peek() {
11945 Token::Comma => {
11946 self.advance();
11947 }
11948 Token::RParen => {
11949 self.advance();
11950 break;
11951 }
11952 other => {
11953 return Err(self.err(alloc::format!(
11954 "expected ',' or ')' in COPY options, got {other:?}"
11955 )));
11956 }
11957 }
11958 }
11959 } else {
11960 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11961 self.parse_one_copy_option(&mut opts)?;
11962 }
11963 }
11964 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11965 return Err(self.err(alloc::format!(
11966 "unexpected token after COPY options: {:?}",
11967 self.peek()
11968 )));
11969 }
11970 Ok(opts)
11971 }
11972
11973 fn parse_one_copy_option(
11974 &mut self,
11975 opts: &mut crate::ast::CopyOptions,
11976 ) -> Result<(), ParseError> {
11977 use crate::ast::CopyFormat;
11978 // The option keyword. NULL lexes as its own token; the rest are
11979 // bare identifiers.
11980 let kw = match self.advance() {
11981 Token::Null => alloc::string::String::from("NULL"),
11982 Token::Ident(s) => s.to_uppercase(),
11983 other => {
11984 return Err(self.err(alloc::format!(
11985 "expected a COPY option keyword, got {other:?}"
11986 )));
11987 }
11988 };
11989 match kw.as_str() {
11990 "FORMAT" => {
11991 let fmt = self.expect_ident_like()?;
11992 match fmt.to_ascii_uppercase().as_str() {
11993 "CSV" => opts.format = CopyFormat::Csv,
11994 "TEXT" => opts.format = CopyFormat::Text,
11995 other => {
11996 return Err(self.err(alloc::format!(
11997 "COPY format \"{}\" not recognized",
11998 other.to_ascii_lowercase()
11999 )));
12000 }
12001 }
12002 }
12003 // Legacy bare format keywords.
12004 "CSV" => opts.format = CopyFormat::Csv,
12005 "TEXT" => opts.format = CopyFormat::Text,
12006 "HEADER" => {
12007 opts.header = match self.peek() {
12008 Token::True => {
12009 self.advance();
12010 true
12011 }
12012 Token::False => {
12013 self.advance();
12014 false
12015 }
12016 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12017 self.advance();
12018 true
12019 }
12020 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12021 self.advance();
12022 false
12023 }
12024 // Bare HEADER (no boolean) means HEADER true.
12025 _ => true,
12026 };
12027 }
12028 // r1066 (7.38 S5.1) — pgbench 14+ loads with
12029 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12030 // vacuum bookkeeping on a freshly created/truncated
12031 // table; SPG's per-statement visibility makes it a
12032 // faithful no-op, and rejecting it aborted `pgbench -i`
12033 // against the drop-in. Accept ON/OFF/bare, change nothing.
12034 "FREEZE" => match self.peek() {
12035 Token::True | Token::False => {
12036 self.advance();
12037 }
12038 Token::Ident(s)
12039 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12040 {
12041 self.advance();
12042 }
12043 _ => {}
12044 },
12045 "DELIMITER" | "QUOTE" | "ESCAPE" => {
12046 let s = match self.advance() {
12047 Token::String(s) => s,
12048 other => {
12049 return Err(self.err(alloc::format!(
12050 "COPY {kw} expects a single-character string, got {other:?}"
12051 )));
12052 }
12053 };
12054 // v7.39 (round 247) — PG's wording (0A000), keyword in
12055 // lowercase: "COPY delimiter must be a single one-byte
12056 // character".
12057 let one_byte_err = || {
12058 self.err(alloc::format!(
12059 "COPY {} must be a single one-byte character",
12060 kw.to_ascii_lowercase()
12061 ))
12062 };
12063 let mut chars = s.chars();
12064 let c = chars.next().ok_or_else(one_byte_err)?;
12065 if chars.next().is_some() || c.len_utf8() != 1 {
12066 return Err(one_byte_err());
12067 }
12068 match kw.as_str() {
12069 "DELIMITER" => opts.delimiter = Some(c),
12070 "QUOTE" => opts.quote = Some(c),
12071 _ => opts.escape = Some(c),
12072 }
12073 }
12074 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12075 "FORCE_QUOTE" => {
12076 if matches!(self.peek(), Token::Star) {
12077 self.advance();
12078 opts.force_quote = Some(Vec::new());
12079 } else {
12080 if !matches!(self.peek(), Token::LParen) {
12081 return Err(self.err(alloc::format!(
12082 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12083 self.peek()
12084 )));
12085 }
12086 self.advance();
12087 let mut cols = Vec::new();
12088 loop {
12089 cols.push(self.expect_ident_like()?);
12090 match self.peek() {
12091 Token::Comma => {
12092 self.advance();
12093 }
12094 Token::RParen => {
12095 self.advance();
12096 break;
12097 }
12098 other => {
12099 return Err(self.err(alloc::format!(
12100 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12101 )));
12102 }
12103 }
12104 }
12105 opts.force_quote = Some(cols);
12106 }
12107 }
12108 "NULL" => {
12109 opts.null_str = Some(match self.advance() {
12110 Token::String(s) => s,
12111 other => {
12112 return Err(self.err(alloc::format!(
12113 "COPY NULL expects a quoted string, got {other:?}"
12114 )));
12115 }
12116 });
12117 }
12118 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12119 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12120 // FORCE_NULL too.
12121 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12122 let cols = self.parse_copy_column_list(&kw)?;
12123 if kw == "FORCE_NOT_NULL" {
12124 opts.force_not_null = Some(cols);
12125 } else {
12126 opts.force_null = Some(cols);
12127 }
12128 }
12129 other => {
12130 // PG's wording, lowercased option name.
12131 return Err(self.err(alloc::format!(
12132 "option \"{}\" not recognized",
12133 other.to_ascii_lowercase()
12134 )));
12135 }
12136 }
12137 Ok(())
12138 }
12139
12140 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12141 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12142 /// is the `*` spelling.
12143 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12144 if matches!(self.peek(), Token::Star) {
12145 self.advance();
12146 return Ok(Vec::new());
12147 }
12148 if !matches!(self.peek(), Token::LParen) {
12149 return Err(self.err(alloc::format!(
12150 "expected '(' or '*' after {kw}, got {:?}",
12151 self.peek()
12152 )));
12153 }
12154 self.advance();
12155 let mut cols = Vec::new();
12156 loop {
12157 cols.push(self.expect_ident_like()?);
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 {kw} list, got {other:?}"
12169 )));
12170 }
12171 }
12172 }
12173 Ok(cols)
12174 }
12175
12176 fn parse_partition_bounds_tail(
12177 &mut self,
12178 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12179 use crate::ast::PartitionOfBoundsAst;
12180 match self.peek() {
12181 Token::Default => {
12182 self.advance();
12183 Ok(PartitionOfBoundsAst::Default)
12184 }
12185 Token::For => {
12186 self.advance();
12187 if !matches!(self.peek(), Token::Values) {
12188 return Err(
12189 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12190 );
12191 }
12192 self.advance();
12193 let want_with = matches!(
12194 self.peek(),
12195 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12196 );
12197 if want_with {
12198 self.advance();
12199 if !matches!(self.peek(), Token::LParen) {
12200 return Err(self.err(format!(
12201 "expected '(' after FOR VALUES WITH, got {:?}",
12202 self.peek()
12203 )));
12204 }
12205 self.advance();
12206 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12207 loop {
12208 let key = self.expect_ident_like()?;
12209 let n = match self.peek().clone() {
12210 Token::Integer(v) if u32::try_from(v).is_ok() => {
12211 self.advance();
12212 v as u32
12213 }
12214 other => {
12215 return Err(self.err(format!(
12216 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12217 )));
12218 }
12219 };
12220 match key.to_ascii_uppercase().as_str() {
12221 "MODULUS" => modulus = Some(n),
12222 "REMAINDER" => remainder = Some(n),
12223 other => {
12224 return Err(self.err(format!(
12225 "FOR VALUES WITH: unknown key {other:?}; \
12226 expected MODULUS or REMAINDER"
12227 )));
12228 }
12229 }
12230 match self.peek() {
12231 Token::Comma => {
12232 self.advance();
12233 }
12234 Token::RParen => {
12235 self.advance();
12236 break;
12237 }
12238 other => {
12239 return Err(self.err(format!(
12240 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12241 )));
12242 }
12243 }
12244 }
12245 let modulus = modulus
12246 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12247 let remainder = remainder.ok_or_else(|| {
12248 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12249 })?;
12250 if modulus == 0 {
12251 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12252 }
12253 if remainder >= modulus {
12254 return Err(self.err(format!(
12255 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12256 )));
12257 }
12258 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12259 }
12260 match self.peek() {
12261 Token::From => {
12262 self.advance();
12263 let lower = Box::new(self.parse_partition_bound_expr()?);
12264 if !matches!(self.peek(), Token::To) {
12265 return Err(self.err(format!(
12266 "expected TO after FROM (...), got {:?}",
12267 self.peek()
12268 )));
12269 }
12270 self.advance();
12271 let upper = Box::new(self.parse_partition_bound_expr()?);
12272 Ok(PartitionOfBoundsAst::Range { lower, upper })
12273 }
12274 Token::In => {
12275 self.advance();
12276 if !matches!(self.peek(), Token::LParen) {
12277 return Err(self.err(format!(
12278 "expected '(' after FOR VALUES IN, got {:?}",
12279 self.peek()
12280 )));
12281 }
12282 self.advance();
12283 let mut values = Vec::new();
12284 loop {
12285 values.push(self.parse_expr(0)?);
12286 match self.peek() {
12287 Token::Comma => {
12288 self.advance();
12289 }
12290 Token::RParen => {
12291 self.advance();
12292 break;
12293 }
12294 other => {
12295 return Err(self.err(format!(
12296 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12297 )));
12298 }
12299 }
12300 }
12301 if values.is_empty() {
12302 return Err(
12303 self.err("FOR VALUES IN requires at least one literal".to_string())
12304 );
12305 }
12306 Ok(PartitionOfBoundsAst::List { values })
12307 }
12308 other => Err(self.err(format!(
12309 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12310 ))),
12311 }
12312 }
12313 other => Err(self.err(format!(
12314 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12315 ))),
12316 }
12317 }
12318
12319 /// v7.16.2 — peek for `information_schema.<tbl>` /
12320 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12321 /// three tokens + return a synthetic table name the engine's
12322 /// SELECT path recognises as a virtual view. Returns `None`
12323 /// when the head doesn't look like a meta-qualified name.
12324 /// Used by `parse_table_ref` to bypass the
12325 /// `expect_ident_like` schema-strip for these specific PG
12326 /// meta schemas (mailrs round-10 A.3).
12327 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12328 // Extract the schema name. Must be a plain ident token.
12329 let schema = match self.tokens.get(self.pos) {
12330 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12331 _ => return None,
12332 };
12333 // Dot.
12334 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12335 return None;
12336 }
12337 // The table-side ident may lex as a reserved keyword
12338 // (e.g. `Token::Tables`). Tolerate the common ones via a
12339 // helper that reads the trailing token's underlying name.
12340 let tbl = match self.tokens.get(self.pos + 2)? {
12341 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12342 Token::Tables => "tables".to_string(),
12343 // Other PG meta table names that may collide with
12344 // reserved keywords land here as needed.
12345 _ => return None,
12346 };
12347 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12348 // names so the synthetic name doesn't double-prefix
12349 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12350 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12351 ("__spg_info_", tbl.to_ascii_lowercase())
12352 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12353 // v7.39 (round 541) — only the catalogs SPG actually
12354 // synthesises are rewritten, which is what the BARE path
12355 // has always checked. Anything else keeps its own name and
12356 // takes the ordinary route: `pg_stat_activity` and friends
12357 // resolve through meta_view_result, and a name that is no
12358 // catalog at all gets PG's "relation does not exist"
12359 // instead of a message about a view SPG cannot materialise.
12360 let lowered = tbl.to_ascii_lowercase();
12361 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12362 self.advance(); // schema
12363 self.advance(); // dot
12364 self.advance(); // tbl
12365 return Some((lowered.clone(), lowered));
12366 }
12367 let bare = lowered
12368 .strip_prefix("pg_")
12369 .map(alloc::string::String::from)
12370 .unwrap_or(lowered);
12371 ("__spg_pg_", bare)
12372 } else if schema.eq_ignore_ascii_case("mysql") {
12373 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12374 // (`mysql.user`, `mysql.db`). Same synthetic-name
12375 // shape as pg_catalog.
12376 ("__spg_mysql_", tbl.to_ascii_lowercase())
12377 } else {
12378 return None;
12379 };
12380 self.advance(); // schema
12381 self.advance(); // dot
12382 self.advance(); // tbl
12383 Some((
12384 alloc::format!("{prefix}{normalised}"),
12385 tbl.to_ascii_lowercase(),
12386 ))
12387 }
12388
12389 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12390 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12391 /// implicit front of every search_path, so a bare reference to a
12392 /// known catalog table always means the catalog table. Only the
12393 /// names the engine actually synthesises are recognised — any
12394 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12395 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12396 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12397 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12398 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12399 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12400 // through the meta_view_result path instead, and already resolve
12401 // bare — they must NOT be listed here or the __spg_ rewrite would
12402 // mis-target them.)
12403 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12404 let name = match self.tokens.get(self.pos) {
12405 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12406 _ => return None,
12407 };
12408 // A following dot means this ident is a schema qualifier,
12409 // not a table name — let the qualified path handle it.
12410 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12411 return None;
12412 }
12413 if !PG_META_TABLES.contains(&name.as_str()) {
12414 return None;
12415 }
12416 self.advance();
12417 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12418 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12419 }
12420
12421 /// Consume a bare ident if its lowercase matches `kw`, else err.
12422 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12423 /// Peeks only; the caller advances.
12424 fn peek_keyword_ident(&self, kw: &str) -> bool {
12425 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12426 }
12427
12428 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12429 match self.advance() {
12430 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12431 other => Err(ParseError {
12432 message: format!("expected {kw:?}, got {other:?}"),
12433 token_pos: self.consumed_pos(),
12434 }),
12435 }
12436 }
12437
12438 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12439 /// literal (`'foo'`) — same shape used by CREATE USER for the
12440 /// username slot.
12441 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12442 match self.advance() {
12443 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12444 other => Err(ParseError {
12445 message: format!("expected identifier or string, got {other:?}"),
12446 token_pos: self.consumed_pos(),
12447 }),
12448 }
12449 }
12450
12451 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12452 match self.advance() {
12453 Token::String(s) => Ok(s),
12454 other => Err(ParseError {
12455 message: format!("expected quoted string, got {other:?}"),
12456 token_pos: self.consumed_pos(),
12457 }),
12458 }
12459 }
12460
12461 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12462 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12463 // subqueries recurse through here without passing
12464 // parse_expr; share the same nesting budget.
12465 self.enter_nested()?;
12466 let r = self.parse_select_stmt_inner();
12467 self.nest_depth -= 1;
12468 r
12469 }
12470
12471 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12472 // Caller dispatches on Token::Select; the inner helper handles
12473 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12474 // get a fresh bare-select parse and may not have their own ORDER
12475 // BY / LIMIT.
12476 let mut head = self.parse_bare_select()?;
12477 let into = self.pending_select_into.take();
12478 self.parse_setop_chain_into(&mut head)?;
12479 self.parse_select_tail_into(&mut head)?;
12480 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12481 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12482 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12483 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12484 // to the body, as it does in PostgreSQL.
12485 if let Some((name, temporary)) = into {
12486 return Ok(Statement::CreateMaterializedView(
12487 crate::ast::CreateMaterializedViewStatement {
12488 temporary,
12489 name,
12490 if_not_exists: false,
12491 columns: Vec::new(),
12492 body: head,
12493 with_data: true,
12494 as_plain_table: true,
12495 },
12496 ));
12497 }
12498 Ok(Statement::Select(head))
12499 }
12500
12501 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12502 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12503 /// token), and INTERSECT [ALL] (a bare ident — it was never
12504 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12505 /// tighter than UNION / EXCEPT — the executor folds the chain
12506 /// left-to-right, which is already correct for LEADING
12507 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12508 /// pair nests into that previous peer, so A UNION B INTERSECT C
12509 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12510 /// groups.
12511 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12512 // A parenthesized group arrives with its own (already
12513 // regrouped) unions on `head`; only the pairs THIS chain
12514 // appends participate in the precedence regroup below —
12515 // nesting an outer INTERSECT into a group-internal peer
12516 // would dissolve the explicit grouping.
12517 let boundary = head.unions.len();
12518 loop {
12519 let base = match self.peek() {
12520 Token::Union => UnionKind::Distinct,
12521 Token::Except => UnionKind::Except,
12522 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12523 _ => break,
12524 };
12525 self.advance();
12526 let kind = if matches!(self.peek(), Token::All) {
12527 self.advance();
12528 match base {
12529 UnionKind::Distinct => UnionKind::All,
12530 UnionKind::Except => UnionKind::ExceptAll,
12531 _ => UnionKind::IntersectAll,
12532 }
12533 } else {
12534 base
12535 };
12536 let peer = self.parse_bare_select()?;
12537 head.unions.push((kind, peer));
12538 }
12539 let mut pairs = core::mem::take(&mut head.unions);
12540 let tail = pairs.split_off(boundary);
12541 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12542 for (kind, peer) in tail {
12543 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12544 // An intersect nests into the previous element of THIS
12545 // chain only; with no new previous element it stays at
12546 // the outer level (the left fold applies it to the
12547 // whole head, group included).
12548 match (
12549 is_intersect,
12550 regrouped.len() > boundary,
12551 regrouped.last_mut(),
12552 ) {
12553 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12554 _ => regrouped.push((kind, peer)),
12555 }
12556 }
12557 head.unions = regrouped;
12558 Ok(())
12559 }
12560
12561 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12562 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12563 /// the top-level bare VALUES statement reuses it verbatim.
12564 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12565 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12566 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12567 /// where the grouping-set universe is still in scope.
12568 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12569 if !matches!(self.peek(), Token::Order) {
12570 return Ok(Vec::new());
12571 }
12572 self.advance();
12573 if !self.peek_is_by() {
12574 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12575 }
12576 self.advance();
12577 let mut keys = Vec::new();
12578 loop {
12579 // v7.39 (round 691) — save/restore, the discipline this parser
12580 // already uses around `pending_sample_preds`, so a subquery inside
12581 // a key neither inherits nor leaks the channel.
12582 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12583 let saved_coll = self.order_key_collation.take();
12584 let parsed = self.parse_expr(0);
12585 self.in_order_by_key = saved_flag;
12586 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12587 let expr = parsed?;
12588 let desc = if matches!(self.peek(), Token::Desc) {
12589 self.advance();
12590 true
12591 } else if matches!(self.peek(), Token::Asc) {
12592 self.advance();
12593 false
12594 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12595 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12596 // one ordering per type, so the btree comparison operators map
12597 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12598 // would need a custom operator class — honest error.
12599 self.advance();
12600 match self.advance() {
12601 Token::Lt | Token::LtEq => false,
12602 Token::Gt | Token::GtEq => true,
12603 other => {
12604 return Err(self.err(alloc::format!(
12605 "ORDER BY USING supports the btree comparison \
12606 operators (< <= > >=); got {other:?}"
12607 )));
12608 }
12609 }
12610 } else {
12611 false
12612 };
12613 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12614 let nulls_first = self.parse_optional_nulls_placement()?;
12615 keys.push(OrderBy {
12616 expr,
12617 desc,
12618 nulls_first,
12619 collation,
12620 });
12621 if matches!(self.peek(), Token::Comma) {
12622 self.advance();
12623 } else {
12624 break;
12625 }
12626 }
12627 Ok(keys)
12628 }
12629
12630 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12631 // v7.39 (round 135) — a grouping-set query may have already parsed +
12632 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12633 // no ORDER BY token is present, keep that pre-set order_by rather than
12634 // clobbering it with an empty list.
12635 let parsed_keys = self.parse_order_by_keys()?;
12636 head.order_by = if parsed_keys.is_empty() {
12637 core::mem::take(&mut head.order_by)
12638 } else {
12639 parsed_keys
12640 };
12641 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12642 // order. PG's grammar takes a limit clause and an offset clause
12643 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12644 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12645 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12646 // spelling died on `expected end of input, got Limit`.
12647 //
12648 // Each may appear at most once, and LIMIT and FETCH FIRST are
12649 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12650 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12651 // A second one is left unconsumed here, which the caller reports
12652 // as trailing input rather than silently taking the last.
12653 let mut saw_limit = false;
12654 let mut saw_offset = false;
12655 loop {
12656 if !saw_limit && matches!(self.peek(), Token::Limit) {
12657 self.advance();
12658 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12659 // PG synonyms for "no limit". Treat both as None
12660 // (no head.limit set) so the engine's existing
12661 // unlimited-result path takes over. Reject was the
12662 // pre-5.1 behaviour and broke pg_dump-flavoured
12663 // tooling that occasionally emits LIMIT NULL.
12664 if self.consume_limit_unbounded_sentinel() {
12665 head.limit = None;
12666 } else {
12667 let first = self.parse_limit_expr("LIMIT")?;
12668 // MySQL `LIMIT offset, count` — the first number is
12669 // the offset when a comma follows.
12670 if matches!(self.peek(), Token::Comma) {
12671 self.advance();
12672 let count = self.parse_limit_expr("LIMIT")?;
12673 head.offset = Some(first);
12674 saw_offset = true;
12675 head.limit = Some(count);
12676 } else {
12677 head.limit = Some(first);
12678 }
12679 }
12680 saw_limit = true;
12681 continue;
12682 }
12683 if !saw_offset && matches!(self.peek(), Token::Offset) {
12684 self.advance();
12685 // PG also accepts an optional `ROW` / `ROWS` trailer
12686 // after the offset value (`OFFSET 10 ROWS`). The
12687 // FETCH-FIRST branch below relies on the same.
12688 let off = self.parse_limit_expr("OFFSET")?;
12689 self.consume_optional_rows_keyword();
12690 head.offset = Some(off);
12691 saw_offset = true;
12692 continue;
12693 }
12694 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12695 // the SQL-standard alias for LIMIT. PG accepts both
12696 // spellings interchangeably; pg_dump emits FETCH FIRST in
12697 // newer versions. We map it onto `head.limit` so the
12698 // engine path is unified.
12699 if !saw_limit
12700 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12701 if s.eq_ignore_ascii_case("fetch"))
12702 {
12703 self.advance(); // FETCH
12704 // `FIRST` or `NEXT` (both legal per SQL standard).
12705 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12706 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12707 {
12708 self.advance();
12709 }
12710 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12711 // implicit 1 — but we always consume one if present).
12712 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12713 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12714 {
12715 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12716 crate::ast::LimitExpr::Literal(1)
12717 } else {
12718 self.parse_limit_expr("FETCH FIRST")?
12719 };
12720 // Eat `ROW` / `ROWS` if not already consumed above.
12721 self.consume_optional_rows_keyword();
12722 // Optional `ONLY` (the spec form) — or the SQL:2008
12723 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12724 // now honours WITH TIES by extending past the LIMIT
12725 // truncation point through every row that shares the
12726 // last-kept row's ORDER BY key.
12727 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12728 if s.eq_ignore_ascii_case("only"))
12729 {
12730 self.advance();
12731 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12732 if s.eq_ignore_ascii_case("with"))
12733 {
12734 self.advance(); // WITH
12735 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12736 if s.eq_ignore_ascii_case("ties"))
12737 {
12738 self.advance();
12739 head.limit_with_ties = true;
12740 }
12741 }
12742 head.limit = Some(count);
12743 saw_limit = true;
12744 continue;
12745 }
12746 break;
12747 }
12748 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12749 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12750 // [ OF table_name [, …] ]
12751 // [ NOWAIT | SKIP LOCKED ]
12752 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12753 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12754 // SELECT already returns a consistent snapshot — so these
12755 // are accept-and-discard: the parser absorbs them so
12756 // mailrs / Rails / Django code paths that emit `SELECT
12757 // … FOR UPDATE` for advisory pessimistic locking load
12758 // without a parser error. The on-disk locking model is
12759 // unchanged; callers that rely on FOR UPDATE for read-
12760 // through-write ordering still get the right answer
12761 // because SPG serialises writes anyway.
12762 head.locking = self
12763 .consume_optional_for_lock_clauses()
12764 .map(alloc::boxed::Box::new);
12765 Ok(())
12766 }
12767
12768 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12769 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12770 /// LOCKED ]` trailers. Each clause is fully accepted and
12771 /// discarded — SPG's single-writer model already satisfies the
12772 /// callers' implicit ordering requirement. Stops at the first
12773 /// token that isn't `FOR`.
12774 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12775 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12776 // not discarded. PG keeps the strongest of several clauses; the
12777 // policy of the last one wins, which is what this loop records.
12778 let mut seen: Option<crate::ast::LockingClause> = None;
12779 while matches!(self.peek(), Token::For) {
12780 // v7.37.14 (A2.5-stub) — record that this query asked
12781 // for a row lock the parser is about to silently
12782 // discard. Operators surface the count via
12783 // `spg_sql::silent_for_update_count()` so they can
12784 // gauge how much of the workload depends on advisory
12785 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12786 // before v7.37.15's per-row tuple locking lands.
12787 crate::record_silent_for_update_clause();
12788 self.advance(); // FOR
12789 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12790 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12791 let mut no_key = false;
12792 let mut key = false;
12793 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12794 if s.eq_ignore_ascii_case("no"))
12795 {
12796 self.advance(); // NO
12797 no_key = true;
12798 // The next ident should be KEY but be generous;
12799 // anything followed by UPDATE/SHARE is accepted.
12800 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12801 if s.eq_ignore_ascii_case("key"))
12802 {
12803 self.advance(); // KEY
12804 }
12805 }
12806 // `KEY` prefix (PG `FOR KEY SHARE`).
12807 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12808 if s.eq_ignore_ascii_case("key"))
12809 {
12810 self.advance(); // KEY
12811 key = true;
12812 }
12813 // Lock-strength keyword: UPDATE / SHARE. Required, but
12814 // we're lenient — an unexpected token here just bails
12815 // (we already consumed FOR; caller's downstream
12816 // dispatch will error if anything actually depends on
12817 // the trailing tokens).
12818 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12819 if s.eq_ignore_ascii_case("update"));
12820 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12821 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12822 {
12823 self.advance();
12824 use crate::ast::LockStrength as LS;
12825 let strength = match (is_update, no_key, key) {
12826 (true, true, _) => LS::NoKeyUpdate,
12827 (true, _, _) => LS::Update,
12828 (false, _, true) => LS::KeyShare,
12829 (false, _, _) => LS::Share,
12830 };
12831 seen = Some(crate::ast::LockingClause {
12832 strength,
12833 of_tables: alloc::vec::Vec::new(),
12834 policy: crate::ast::LockWait::Wait,
12835 });
12836 } else {
12837 // FOR by itself (or `FOR KEY` with nothing after) —
12838 // give up on the lock-clause path. We've already
12839 // advanced past FOR; further attempts to parse
12840 // here would clobber state.
12841 return seen;
12842 }
12843 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12844 // joining and locking only a subset of tables.
12845 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12846 if s.eq_ignore_ascii_case("of"))
12847 {
12848 self.advance(); // OF
12849 #[allow(clippy::while_let_loop)]
12850 loop {
12851 match self.peek() {
12852 Token::Ident(_) | Token::QuotedIdent(_) => {
12853 // v7.39 (round 294) — the name is CAPTURED now: PG
12854 // validates it against the FROM clause, and an
12855 // uncaptured list silently means "lock everything".
12856 let mut nm = match self.advance() {
12857 Token::Ident(n) | Token::QuotedIdent(n) => n,
12858 _ => alloc::string::String::new(),
12859 };
12860 // Optional schema-qualified `schema.table`.
12861 if matches!(self.peek(), Token::Dot) {
12862 self.advance();
12863 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12864 {
12865 self.advance();
12866 nm = n;
12867 }
12868 }
12869 if let Some(c) = seen.as_mut() {
12870 c.of_tables.push(nm);
12871 }
12872 }
12873 _ => break,
12874 }
12875 if matches!(self.peek(), Token::Comma) {
12876 self.advance();
12877 } else {
12878 break;
12879 }
12880 }
12881 }
12882 // Optional `NOWAIT` | `SKIP LOCKED`.
12883 match self.peek().clone() {
12884 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12885 self.advance();
12886 if let Some(c) = seen.as_mut() {
12887 c.policy = crate::ast::LockWait::NoWait;
12888 }
12889 }
12890 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12891 self.advance(); // SKIP
12892 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12893 if s.eq_ignore_ascii_case("locked"))
12894 {
12895 self.advance(); // LOCKED
12896 if let Some(c) = seen.as_mut() {
12897 c.policy = crate::ast::LockWait::SkipLocked;
12898 }
12899 }
12900 }
12901 _ => {}
12902 }
12903 // Loop: PG allows multiple FOR clauses chained.
12904 }
12905 seen
12906 }
12907
12908 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12909 /// Bind value gets resolved during prepared-statement Execute;
12910 /// the Pratt expression parser would over-accept here (e.g.
12911 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12912 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12913 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12914 /// when one was consumed; caller skips the regular
12915 /// limit-value parse and leaves `head.limit` at None.
12916 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12917 if matches!(self.peek(), Token::Null) {
12918 self.advance();
12919 return true;
12920 }
12921 if matches!(self.peek(), Token::All) {
12922 self.advance();
12923 return true;
12924 }
12925 false
12926 }
12927
12928 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12929 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12930 /// SQL-standard shape. No-op when missing.
12931 fn consume_optional_rows_keyword(&mut self) {
12932 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12933 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12934 {
12935 self.advance();
12936 }
12937 }
12938
12939 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12940 ///
12941 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12942 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12943 /// constant, which is why that spelling keeps the token path below.
12944 ///
12945 /// Constants are folded here rather than carried into the tree: the
12946 /// 15+ execution paths that read the row count go through
12947 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12948 /// means "no limit". A clause the engine could not resolve would
12949 /// therefore return the WHOLE table instead of failing. Folding at
12950 /// parse time keeps that impossible; a non-constant clause is still
12951 /// a clean error (recorded residual — closing it wants a resolution
12952 /// pre-pass on the simple-query path, where `substitute_placeholders`
12953 /// does not run).
12954 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12955 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12956 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12957 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12958 // ONLY` both work (its grammar takes a c_expr). Both measured
12959 // against PG 18.4 in round 305.
12960 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12961 return self.parse_limit_constant(label);
12962 }
12963 // One pass, no rewind: `advance()` takes each token by
12964 // `mem::replace`, so a consumed token reads back as Eof and this
12965 // parser cannot backtrack. Everything — bare literal included —
12966 // is therefore folded from the parsed expression rather than
12967 // re-read from the token stream.
12968 let start = self.pos;
12969 let e = self.parse_expr(0)?;
12970 if let crate::ast::Expr::Placeholder(n) = e {
12971 return Ok(crate::ast::LimitExpr::Placeholder(n));
12972 }
12973 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12974 match fold_limit_constant(&e) {
12975 Some(Ok(v)) if v < 0 => Err(ParseError {
12976 message: alloc::format!("{neg_label} must not be negative"),
12977 token_pos: start,
12978 }),
12979 Some(Ok(v)) => u32::try_from(v)
12980 .map(crate::ast::LimitExpr::Literal)
12981 .map_err(|_| ParseError {
12982 message: alloc::format!("{label} value too large: {v}"),
12983 token_pos: start,
12984 }),
12985 Some(Err(message)) => Err(ParseError {
12986 message: message.replace("{L}", neg_label),
12987 token_pos: start,
12988 }),
12989 // v7.39 (round 305, V23) — not foldable at parse time
12990 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12991 // expression; the engine evaluates it once before dispatch.
12992 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12993 }
12994 }
12995
12996 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12997 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12998 // coercion rules, not just an integer token: a NUMERIC rounds half
12999 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13000 // refused with PG's wording ("LIMIT must not be negative", 2201W /
13001 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13002 // content, failing as an input-syntax error on the value. General
13003 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13004 // they need an Expr-carrying LimitExpr variant.
13005 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13006 let err_at = |message: alloc::string::String, pos: usize| ParseError {
13007 message,
13008 token_pos: pos,
13009 };
13010 match self.advance() {
13011 Token::Integer(n) if n >= 0 => u32::try_from(n)
13012 .map(crate::ast::LimitExpr::Literal)
13013 .map_err(|_| ParseError {
13014 message: alloc::format!("{label} value too large: {n}"),
13015 token_pos: self.consumed_pos(),
13016 }),
13017 Token::Integer(_) => Err(err_at(
13018 alloc::format!("{neg_label} must not be negative"),
13019 self.pos.saturating_sub(1),
13020 )),
13021 Token::Numeric(t) => {
13022 let pos = self.pos.saturating_sub(1);
13023 let v: f64 = t.parse().map_err(|_| {
13024 err_at(
13025 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13026 pos,
13027 )
13028 })?;
13029 if v < 0.0 {
13030 return Err(err_at(
13031 alloc::format!("{neg_label} must not be negative"),
13032 pos,
13033 ));
13034 }
13035 // Round half away from zero — PG's numeric→bigint cast.
13036 // (no_std: no f64::round; v is non-negative, so truncating
13037 // v + 0.5 is the same thing.)
13038 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13039 let rounded = (v + 0.5) as u64;
13040 u32::try_from(rounded)
13041 .map(crate::ast::LimitExpr::Literal)
13042 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13043 }
13044 Token::Minus => {
13045 let pos = self.pos.saturating_sub(1);
13046 match self.peek() {
13047 Token::Integer(_) | Token::Numeric(_) => {
13048 self.advance();
13049 Err(err_at(
13050 alloc::format!("{neg_label} must not be negative"),
13051 pos,
13052 ))
13053 }
13054 other => Err(err_at(
13055 alloc::format!(
13056 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13057 ),
13058 pos,
13059 )),
13060 }
13061 }
13062 Token::String(t) => {
13063 let pos = self.pos.saturating_sub(1);
13064 match t.trim().parse::<i64>() {
13065 Ok(n) if n < 0 => Err(err_at(
13066 alloc::format!("{neg_label} must not be negative"),
13067 pos,
13068 )),
13069 Ok(n) => u32::try_from(n)
13070 .map(crate::ast::LimitExpr::Literal)
13071 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13072 Err(_) => Err(err_at(
13073 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13074 pos,
13075 )),
13076 }
13077 }
13078 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13079 other => Err(ParseError {
13080 message: alloc::format!(
13081 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13082 ),
13083 token_pos: self.consumed_pos(),
13084 }),
13085 }
13086 }
13087
13088 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13089 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13090 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13091 /// `parse_select_stmt` is responsible for filling those in.
13092 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13093 /// call in the expression tree to the per-set integer bitmask
13094 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13095 /// is dropped in this grouping set). Runs during the ROLLUP /
13096 /// CUBE / GROUPING SETS expansion, where the set is known.
13097 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13098 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13099 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13100 if let Expr::FunctionCall { name, .. } = expr
13101 && name.eq_ignore_ascii_case("grouping")
13102 {
13103 if !out.iter().any(|e| e == expr) {
13104 out.push(expr.clone());
13105 }
13106 return;
13107 }
13108 match expr {
13109 Expr::Binary { lhs, rhs, .. } => {
13110 Self::collect_grouping_calls(lhs, out);
13111 Self::collect_grouping_calls(rhs, out);
13112 }
13113 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13114 Self::collect_grouping_calls(expr, out)
13115 }
13116 Expr::FunctionCall { args, .. } => {
13117 for a in args {
13118 Self::collect_grouping_calls(a, out);
13119 }
13120 }
13121 Expr::Case {
13122 operand,
13123 branches,
13124 else_branch,
13125 } => {
13126 if let Some(o) = operand {
13127 Self::collect_grouping_calls(o, out);
13128 }
13129 for (c, v) in branches {
13130 Self::collect_grouping_calls(c, out);
13131 Self::collect_grouping_calls(v, out);
13132 }
13133 if let Some(x) = else_branch {
13134 Self::collect_grouping_calls(x, out);
13135 }
13136 }
13137 _ => {}
13138 }
13139 }
13140
13141 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13142 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13143 /// `__grp_ord_k` (injected per grouping-set branch).
13144 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13145 if let Expr::FunctionCall { name, .. } = expr
13146 && name.eq_ignore_ascii_case("grouping")
13147 {
13148 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13149 *expr = Expr::Column(crate::ast::ColumnName {
13150 qualifier: None,
13151 name: alloc::format!("__grp_ord_{k}"),
13152 });
13153 }
13154 return;
13155 }
13156 match expr {
13157 Expr::Binary { lhs, rhs, .. } => {
13158 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13159 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13160 }
13161 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13162 Self::rewrite_grouping_to_col(expr, grp_exprs)
13163 }
13164 Expr::FunctionCall { args, .. } => {
13165 for a in args {
13166 Self::rewrite_grouping_to_col(a, grp_exprs);
13167 }
13168 }
13169 Expr::Case {
13170 operand,
13171 branches,
13172 else_branch,
13173 } => {
13174 if let Some(o) = operand {
13175 Self::rewrite_grouping_to_col(o, grp_exprs);
13176 }
13177 for (c, v) in branches {
13178 Self::rewrite_grouping_to_col(c, grp_exprs);
13179 Self::rewrite_grouping_to_col(v, grp_exprs);
13180 }
13181 if let Some(x) = else_branch {
13182 Self::rewrite_grouping_to_col(x, grp_exprs);
13183 }
13184 }
13185 _ => {}
13186 }
13187 }
13188
13189 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13190 /// as the list of key sets it contributes. A bare expression is one
13191 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13192 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13193 /// the concatenation of its items' sets, where an item is itself an
13194 /// element, a parenthesized key list, or the empty set `()`. A
13195 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13196 /// move together.
13197 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13198 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13199 // ROLLUP ( … ) / CUBE ( … )
13200 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13201 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13202 {
13203 let is_cube = is_kw(self.peek(), "cube");
13204 self.advance(); // ROLLUP / CUBE
13205 self.advance(); // (
13206 let mut units: Vec<Vec<Expr>> = Vec::new();
13207 loop {
13208 if matches!(self.peek(), Token::LParen) {
13209 // Composite unit: (a, b) rolls up as one.
13210 self.advance();
13211 let mut unit = Vec::new();
13212 if !matches!(self.peek(), Token::RParen) {
13213 loop {
13214 unit.push(self.parse_expr(0)?);
13215 match self.peek() {
13216 Token::Comma => {
13217 self.advance();
13218 }
13219 Token::RParen => break,
13220 other => {
13221 return Err(self.err(format!(
13222 "expected ',' or ')' in grouping unit, got {other:?}"
13223 )));
13224 }
13225 }
13226 }
13227 }
13228 self.advance(); // )
13229 units.push(unit);
13230 } else {
13231 units.push(alloc::vec![self.parse_expr(0)?]);
13232 }
13233 match self.peek() {
13234 Token::Comma => {
13235 self.advance();
13236 }
13237 Token::RParen => break,
13238 other => {
13239 return Err(self.err(format!(
13240 "expected ',' or ')' in grouping list, got {other:?}"
13241 )));
13242 }
13243 }
13244 }
13245 self.advance(); // )
13246 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13247 units
13248 .iter()
13249 .zip(unit_sel.iter())
13250 .filter(|(_, keep)| **keep)
13251 .flat_map(|(u, _)| u.iter().cloned())
13252 .collect()
13253 };
13254 let n = units.len();
13255 if is_cube {
13256 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13257 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13258 .collect();
13259 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13260 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13261 }
13262 return Ok((0..=n)
13263 .rev()
13264 .map(|keep| {
13265 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13266 flatten(&sel)
13267 })
13268 .collect());
13269 }
13270 // GROUPING SETS ( item [, item]* )
13271 if is_kw(self.peek(), "grouping")
13272 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13273 {
13274 self.advance(); // GROUPING
13275 self.advance(); // SETS
13276 if !matches!(self.peek(), Token::LParen) {
13277 return Err(self.err(format!(
13278 "expected '(' after GROUPING SETS, got {:?}",
13279 self.peek()
13280 )));
13281 }
13282 self.advance(); // outer (
13283 let mut sets: Vec<Vec<Expr>> = Vec::new();
13284 loop {
13285 if matches!(self.peek(), Token::LParen) {
13286 // A parenthesized key list (or the empty set).
13287 self.advance();
13288 let mut set = Vec::new();
13289 if !matches!(self.peek(), Token::RParen) {
13290 loop {
13291 set.push(self.parse_expr(0)?);
13292 match self.peek() {
13293 Token::Comma => {
13294 self.advance();
13295 }
13296 Token::RParen => break,
13297 other => {
13298 return Err(self.err(format!(
13299 "expected ',' or ')' in grouping set, got {other:?}"
13300 )));
13301 }
13302 }
13303 }
13304 }
13305 self.advance(); // )
13306 sets.push(set);
13307 } else {
13308 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13309 // bare expression.
13310 sets.extend(self.parse_grouping_element()?);
13311 }
13312 match self.peek() {
13313 Token::Comma => {
13314 self.advance();
13315 }
13316 Token::RParen => break,
13317 other => {
13318 return Err(self.err(format!(
13319 "expected ',' or ')' after a grouping set, got {other:?}"
13320 )));
13321 }
13322 }
13323 }
13324 self.advance(); // outer )
13325 return Ok(sets);
13326 }
13327 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13328 }
13329
13330 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13331 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13332 // set evaluates to NULL, at any depth. Previously only a *top-level*
13333 // select item equal to a dropped key was nullified, so a key nested in
13334 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13335 // column and failed to resolve against the set's synthetic schema.
13336 if dropped.iter().any(|d| d == expr) {
13337 *expr = Expr::Literal(Literal::Null);
13338 return;
13339 }
13340 if let Expr::FunctionCall { name, args } = expr
13341 && name.eq_ignore_ascii_case("grouping")
13342 {
13343 let mut mask: i64 = 0;
13344 for a in args.iter() {
13345 mask <<= 1;
13346 if dropped.iter().any(|d| d == a) {
13347 mask |= 1;
13348 }
13349 }
13350 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13351 // literal: a bare integer in a select item is indistinguishable
13352 // from a positional reference once `ORDER BY 1` substitutes the
13353 // item back in, and the round-232 position check then read the
13354 // mask value as an out-of-range position. The cast changes
13355 // nothing semantically (grouping() is integer).
13356 *expr = Expr::Cast {
13357 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13358 target: crate::ast::CastTarget::Int,
13359 };
13360 return;
13361 }
13362 // Generic recursion over the common expression shapes the
13363 // SELECT list uses; anything without child expressions is
13364 // left alone.
13365 match expr {
13366 Expr::FunctionCall { args, .. } => {
13367 for a in args {
13368 Self::substitute_grouping_calls(a, dropped);
13369 }
13370 }
13371 Expr::Binary { lhs, rhs, .. } => {
13372 Self::substitute_grouping_calls(lhs, dropped);
13373 Self::substitute_grouping_calls(rhs, dropped);
13374 }
13375 Expr::Unary { expr: inner, .. } => {
13376 Self::substitute_grouping_calls(inner, dropped);
13377 }
13378 Expr::Cast { expr: inner, .. } => {
13379 Self::substitute_grouping_calls(inner, dropped);
13380 }
13381 Expr::Case {
13382 operand,
13383 branches,
13384 else_branch,
13385 } => {
13386 if let Some(op) = operand {
13387 Self::substitute_grouping_calls(op, dropped);
13388 }
13389 for (w, t) in branches {
13390 Self::substitute_grouping_calls(w, dropped);
13391 Self::substitute_grouping_calls(t, dropped);
13392 }
13393 if let Some(e) = else_branch {
13394 Self::substitute_grouping_calls(e, dropped);
13395 }
13396 }
13397 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13398 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13399 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13400 // …` is the canonical rollup-total label idiom).
13401 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13402 Expr::Like { expr, pattern, .. } => {
13403 Self::substitute_grouping_calls(expr, dropped);
13404 Self::substitute_grouping_calls(pattern, dropped);
13405 }
13406 Expr::InList { expr, list, .. } => {
13407 Self::substitute_grouping_calls(expr, dropped);
13408 for item in list {
13409 Self::substitute_grouping_calls(item, dropped);
13410 }
13411 }
13412 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13413 Expr::Array(items) => {
13414 for item in items {
13415 Self::substitute_grouping_calls(item, dropped);
13416 }
13417 }
13418 Expr::ArraySubscript { target, index } => {
13419 Self::substitute_grouping_calls(target, dropped);
13420 Self::substitute_grouping_calls(index, dropped);
13421 }
13422 Expr::ArraySlice { target, lo, hi } => {
13423 Self::substitute_grouping_calls(target, dropped);
13424 if let Some(lo) = lo {
13425 Self::substitute_grouping_calls(lo, dropped);
13426 }
13427 if let Some(hi) = hi {
13428 Self::substitute_grouping_calls(hi, dropped);
13429 }
13430 }
13431 Expr::AnyAll { expr, array, .. } => {
13432 Self::substitute_grouping_calls(expr, dropped);
13433 Self::substitute_grouping_calls(array, dropped);
13434 }
13435 _ => {}
13436 }
13437 }
13438
13439 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13440 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13441 // group: `( <select chain> )` usable anywhere a query block
13442 // is (head or peer of an outer chain). The group's own
13443 // unions ride the returned SelectStatement; the executor's
13444 // nested-peer recursion runs them.
13445 if matches!(self.peek(), Token::LParen)
13446 && matches!(
13447 self.tokens.get(self.pos + 1),
13448 Some(Token::Select | Token::LParen | Token::Values)
13449 )
13450 {
13451 self.advance(); // (
13452 self.enter_nested()?;
13453 // v7.37 D.20 — a group whose head is a VALUES list:
13454 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13455 // otherwise recurse into a nested SELECT/group head.
13456 let mut head = (if matches!(self.peek(), Token::Values) {
13457 self.advance(); // VALUES
13458 self.parse_values_rows_body()
13459 } else {
13460 self.parse_bare_select()
13461 })
13462 .and_then(|mut h| {
13463 self.parse_setop_chain_into(&mut h)?;
13464 Ok(h)
13465 });
13466 self.nest_depth -= 1;
13467 let mut head = match &mut head {
13468 Ok(h) => core::mem::take(h),
13469 Err(_) => return head,
13470 };
13471 // v7.37.17 (17.6 siblings) — group-internal tail:
13472 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13473 // group head, then wrap the group as a derived table
13474 // (SELECT * FROM (group)) so the outer chain / outer
13475 // tail can't clobber the group's own ordering or limit.
13476 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13477 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13478 if s.eq_ignore_ascii_case("fetch"));
13479 if has_tail {
13480 self.parse_select_tail_into(&mut head)?;
13481 head = SelectStatement {
13482 locking: None,
13483 ctes: Vec::new(),
13484 distinct: false,
13485 distinct_on: Vec::new(),
13486 items: alloc::vec![SelectItem::Wildcard],
13487 from: Some(FromClause {
13488 primary: TableRef {
13489 name: "subquery".to_string(),
13490 alias: None,
13491 only: false,
13492 as_of_segment: None,
13493 unnest_expr: None,
13494 unnest_column_aliases: Vec::new(),
13495 with_ordinality: false,
13496 generate_series_args: None,
13497 lateral_subquery: Some(Box::new(head)),
13498 jsonb_each_text_arg: None,
13499 table_fn_call: None,
13500 rows_from: None,
13501 json_table: None,
13502 scalar_fn_item: false,
13503 },
13504 joins: Vec::new(),
13505 }),
13506 where_: None,
13507 group_by: None,
13508 group_by_all: false,
13509 having: None,
13510 unions: Vec::new(),
13511 order_by: Vec::new(),
13512 limit: None,
13513 offset: None,
13514 limit_with_ties: false,
13515 window_check_exprs: Vec::new(),
13516 };
13517 }
13518 if !matches!(self.peek(), Token::RParen) {
13519 return Err(self.err(format!(
13520 "expected ')' after parenthesized query group, got {:?}",
13521 self.peek()
13522 )));
13523 }
13524 self.advance();
13525 return Ok(head);
13526 }
13527 // `TABLE name` shorthand as a query block — valid anywhere
13528 // a SELECT head is (set-op peers included).
13529 if matches!(self.peek(), Token::Table)
13530 && matches!(
13531 self.tokens.get(self.pos + 1),
13532 Some(Token::Ident(_) | Token::QuotedIdent(_))
13533 )
13534 {
13535 return self.parse_table_shorthand();
13536 }
13537 if !matches!(self.peek(), Token::Select) {
13538 return Err(self.err(format!(
13539 "expected SELECT to start a query block, got {:?}",
13540 self.peek()
13541 )));
13542 }
13543 self.advance();
13544 let distinct = if matches!(self.peek(), Token::Distinct) {
13545 self.advance();
13546 true
13547 } else {
13548 false
13549 };
13550 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13551 // keep the first row (per ORDER BY) of each group the
13552 // expressions define. Django's .distinct('field') shape.
13553 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13554 self.advance(); // ON
13555 if !matches!(self.peek(), Token::LParen) {
13556 return Err(self.err(format!(
13557 "expected '(' after DISTINCT ON, got {:?}",
13558 self.peek()
13559 )));
13560 }
13561 self.advance();
13562 let mut exprs = Vec::new();
13563 loop {
13564 exprs.push(self.parse_expr(0)?);
13565 match self.peek() {
13566 Token::Comma => {
13567 self.advance();
13568 }
13569 Token::RParen => break,
13570 other => {
13571 return Err(self.err(format!(
13572 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13573 )));
13574 }
13575 }
13576 }
13577 self.advance(); // )
13578 exprs
13579 } else {
13580 Vec::new()
13581 };
13582 let mut items = self.parse_select_list()?;
13583 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13584 // of CTAS. It sits exactly here in PG's grammar, right after the
13585 // target list.
13586 //
13587 // A comment in `ast.rs` has said since v7.38 that CTAS and
13588 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13589 // `SELECT i INTO t FROM src` answered `syntax error at or near
13590 // "INTO"`, which the differential found while measuring what
13591 // PostgreSQL tags each of the five materialising forms with. A
13592 // comment describing a capability the code does not have is the
13593 // defect this version has been finding all day, and this is the
13594 // one it found in the parser.
13595 //
13596 // `INTO` is captured rather than consumed here: the name has to
13597 // travel out of a function that returns a `SelectStatement`, and
13598 // the caller lowers the whole thing to the CTAS node.
13599 if matches!(self.peek(), Token::Into) {
13600 self.advance();
13601 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13602 // the target, not part of its name. SPG has one storage
13603 // class, so `UNLOGGED` is accepted and means nothing, which
13604 // is what it already means on `CREATE TABLE`.
13605 let mut temporary = false;
13606 loop {
13607 match self.peek().clone() {
13608 Token::Ident(w) | Token::QuotedIdent(w)
13609 if w.eq_ignore_ascii_case("temp")
13610 || w.eq_ignore_ascii_case("temporary") =>
13611 {
13612 temporary = true;
13613 self.advance();
13614 }
13615 Token::Ident(w) | Token::QuotedIdent(w)
13616 if w.eq_ignore_ascii_case("unlogged") =>
13617 {
13618 self.advance();
13619 }
13620 Token::Table => {
13621 self.advance();
13622 }
13623 _ => break,
13624 }
13625 }
13626 let name = match self.peek().clone() {
13627 Token::Ident(w) | Token::QuotedIdent(w) => {
13628 self.advance();
13629 w
13630 }
13631 other => {
13632 return Err(self.err(alloc::format!(
13633 "expected a table name after SELECT … INTO, got {other:?}"
13634 )));
13635 }
13636 };
13637 self.pending_select_into = Some((name, temporary));
13638 }
13639 // Scope the TABLESAMPLE lowering channel to this SELECT:
13640 // stash whatever an enclosing select accumulated, collect
13641 // our own FROM's predicates, restore after the combine.
13642 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13643 let mut from = if matches!(self.peek(), Token::From) {
13644 self.advance();
13645 Some(self.parse_from_clause()?)
13646 } else {
13647 None
13648 };
13649 // v7.37 D.22 — a set-returning function in the projection with no FROM
13650 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13651 // rows. Move the first SRF projection item to a FROM-position derived
13652 // table and replace it in the projection with a reference to its output
13653 // column; sibling scalar columns repeat per SRF row. PG names the output
13654 // column after the function (or its AS alias). Reuses the FROM-SRF
13655 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13656 // works via the targetlist-SRF path.
13657 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13658 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13659 // is exactly what the function's own row shape already is. Anywhere else
13660 // (per outer row, or beside other items) it would need a real record-typed
13661 // projection, so it says so rather than answering something else.
13662 if let [
13663 SelectItem::Expr {
13664 expr: Expr::FunctionCall { name, args },
13665 ..
13666 },
13667 ] = items.as_slice()
13668 && name == "__record_expand"
13669 {
13670 let Some(Expr::FunctionCall {
13671 name: inner_name,
13672 args: inner_args,
13673 }) = args.first()
13674 else {
13675 return Err(self.err(
13676 "(<expr>).* expands a function's record — it needs a function call".into(),
13677 ));
13678 };
13679 if from.is_some() {
13680 return Err(self.err(
13681 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13682 .into(),
13683 ));
13684 }
13685 let fn_ref = TableRef {
13686 name: inner_name.clone(),
13687 alias: None,
13688 only: false,
13689 as_of_segment: None,
13690 unnest_expr: None,
13691 unnest_column_aliases: Vec::new(),
13692 with_ordinality: false,
13693 generate_series_args: None,
13694 lateral_subquery: None,
13695 jsonb_each_text_arg: None,
13696 table_fn_call: Some(Box::new((
13697 inner_name.to_ascii_lowercase(),
13698 inner_args.clone(),
13699 ))),
13700 rows_from: None,
13701 json_table: None,
13702 scalar_fn_item: false,
13703 };
13704 items = alloc::vec![SelectItem::Wildcard];
13705 from = Some(FromClause {
13706 primary: fn_ref,
13707 joins: Vec::new(),
13708 });
13709 }
13710 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13711 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13712 // record's fields takes the catalog. It becomes a LATERAL of the same
13713 // function plus one item per declared column — the machinery rounds 65
13714 // and 69 already built.
13715 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13716 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13717 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13718 // express, since the lifted one becomes a scan and the other would
13719 // expand per its rows (a cross product, not a zip). So when the
13720 // projection holds more than one top-level function call, the lift steps
13721 // aside and the engine's target-list expansion takes the whole list.
13722 let fn_call_items = items
13723 .iter()
13724 .filter(|it| {
13725 matches!(
13726 it,
13727 SelectItem::Expr {
13728 expr: Expr::FunctionCall { .. },
13729 ..
13730 }
13731 )
13732 })
13733 .count();
13734 if from.is_none() && fn_call_items <= 1 {
13735 let mut found: Option<(usize, TableRef, String)> = None;
13736 for (i, item) in items.iter().enumerate() {
13737 if let SelectItem::Expr {
13738 expr: Expr::FunctionCall { name, args },
13739 alias,
13740 } = item
13741 {
13742 let lname = name.to_ascii_lowercase();
13743 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13744 let (unnest, gs) = match lname.as_str() {
13745 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13746 "generate_series" if (2..=3).contains(&args.len()) => {
13747 (None, Some(args.clone()))
13748 }
13749 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13750 // no-FROM projection yields the 1-based subscripts, i.e.
13751 // generate_series(1, array_length(arr, dim)); an invalid
13752 // dimension makes array_length NULL → 0 rows, as in PG.
13753 "generate_subscripts" if args.len() == 2 => (
13754 None,
13755 Some(alloc::vec![
13756 Expr::Literal(Literal::Integer(1)),
13757 Expr::FunctionCall {
13758 name: "array_length".to_string(),
13759 args: args.clone(),
13760 },
13761 ]),
13762 ),
13763 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13764 // in a no-FROM projection unnest their *_to_array form.
13765 "string_to_table" | "regexp_split_to_table" => {
13766 let array_fn = if lname == "string_to_table" {
13767 "string_to_array"
13768 } else {
13769 "regexp_split_to_array"
13770 };
13771 (
13772 Some(Box::new(Expr::FunctionCall {
13773 name: array_fn.to_string(),
13774 args: args.clone(),
13775 })),
13776 None,
13777 )
13778 }
13779 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13780 // a no-FROM projection expand per element. The scalar form
13781 // returns the elements as a TEXT array, so unnest over the
13782 // same call materialises one row each (same rewrite the
13783 // FROM-clause form uses).
13784 "jsonb_array_elements"
13785 | "json_array_elements"
13786 | "jsonb_array_elements_text"
13787 | "json_array_elements_text"
13788 if args.len() == 1 =>
13789 {
13790 (
13791 Some(Box::new(Expr::FunctionCall {
13792 name: lname.clone(),
13793 args: args.clone(),
13794 })),
13795 None,
13796 )
13797 }
13798 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13799 // in a no-FROM projection expands per match (scalar form
13800 // returns the matches as a TEXT array → unnest).
13801 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13802 Some(Box::new(Expr::FunctionCall {
13803 name: lname.clone(),
13804 args: args.clone(),
13805 })),
13806 None,
13807 ),
13808 _ => continue,
13809 };
13810 found = Some((
13811 i,
13812 TableRef {
13813 name: colname.clone(),
13814 alias: Some(colname.clone()),
13815 only: false,
13816 as_of_segment: None,
13817 unnest_expr: unnest,
13818 unnest_column_aliases: alloc::vec![colname.clone()],
13819 with_ordinality: false,
13820 generate_series_args: gs,
13821 lateral_subquery: None,
13822 jsonb_each_text_arg: None,
13823 table_fn_call: None,
13824 rows_from: None,
13825 json_table: None,
13826 scalar_fn_item: false,
13827 },
13828 colname,
13829 ));
13830 break;
13831 }
13832 }
13833 if let Some((idx, tref, colname)) = found {
13834 from = Some(FromClause {
13835 primary: tref,
13836 joins: Vec::new(),
13837 });
13838 items[idx] = SelectItem::Expr {
13839 expr: Expr::Column(ColumnName {
13840 qualifier: None,
13841 name: colname.clone(),
13842 }),
13843 alias: Some(colname),
13844 };
13845 }
13846 }
13847 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13848 let where_ = if matches!(self.peek(), Token::Where) {
13849 self.advance();
13850 Some(self.parse_expr(0)?)
13851 } else {
13852 None
13853 };
13854 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13855 Some(match acc {
13856 Some(w) => Expr::Binary {
13857 lhs: Box::new(pred),
13858 op: crate::ast::BinOp::And,
13859 rhs: Box::new(w),
13860 },
13861 None => pred,
13862 })
13863 });
13864 self.pending_sample_preds = enclosing_sample_preds;
13865 let mut group_by_all = false;
13866 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13867 // share one expansion: `grouping_sets` lists the key subsets
13868 // (first = primary, assigned to stmt.group_by; the rest
13869 // become UNION ALL peers), `grouping_universe` is the full
13870 // key list used to compute each peer's dropped keys.
13871 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13872 let mut grouping_universe: Vec<Expr> = Vec::new();
13873 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13874 // A BOOL, not the key list: this frame is the statement parser's, and
13875 // round 430 measured that a `Vec` local here is enough on its own to
13876 // tip the 512 KiB nesting guard. The keys are recoverable from
13877 // `grouping_universe`, which a rollup fills with exactly them.
13878 let mut mysql_rollup = false;
13879 let group_by = if matches!(self.peek(), Token::Group) {
13880 self.advance();
13881 if !self.peek_is_by() {
13882 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13883 }
13884 self.advance();
13885 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13886 // every non-aggregate SELECT-list item later.
13887 if matches!(self.peek(), Token::All) {
13888 self.advance();
13889 group_by_all = true;
13890 None
13891 } else {
13892 // v7.39 (round 242) — PG's general grouping-element grammar:
13893 // GROUP BY [DISTINCT] element [, element]*, where an element
13894 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13895 // SETS (…) — mixed freely. Each element yields a list of
13896 // key sets; the query's grouping sets are the CARTESIAN
13897 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13898 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13899 // content. ROLLUP/CUBE members may be composite
13900 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13901 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13902 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13903 // clause.
13904 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13905 self.advance();
13906 true
13907 } else {
13908 false
13909 };
13910 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13911 loop {
13912 element_sets.push(self.parse_grouping_element()?);
13913 if matches!(self.peek(), Token::Comma) {
13914 self.advance();
13915 } else {
13916 break;
13917 }
13918 }
13919 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13920 for el in &element_sets {
13921 let mut next: Vec<Vec<Expr>> = Vec::new();
13922 for base in &total {
13923 for set in el {
13924 let mut merged = base.clone();
13925 for k in set {
13926 if !merged.iter().any(|m| m == k) {
13927 merged.push(k.clone());
13928 }
13929 }
13930 next.push(merged);
13931 }
13932 }
13933 total = next;
13934 }
13935 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13936 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13937 // The keys and the aggregates come out identical; the ROW
13938 // ORDER does not, and that is the part a report depends on.
13939 // MySQL interleaves each group's subtotal right after its
13940 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13941 // where the union-of-grouping-sets expansion emits every
13942 // leaf first and then every subtotal. MariaDB REFUSES an
13943 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13944 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13945 // agree on the order and disagree only on whether ORDER BY
13946 // is allowed (MySQL allows it; SPG allows it too, since
13947 // refusing would break the clients that can write it).
13948 if self.mysql_dialect
13949 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13950 && matches!(
13951 self.tokens.get(self.pos + 1),
13952 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13953 )
13954 {
13955 self.advance(); // WITH
13956 self.advance(); // ROLLUP
13957 let keys = total.into_iter().next().unwrap_or_default();
13958 mysql_rollup = true;
13959 // n+1 prefixes, largest first — the same expansion
13960 // `ROLLUP (…)` produces.
13961 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13962 }
13963 if distinct_sets {
13964 let mut seen: Vec<Vec<String>> = Vec::new();
13965 total.retain(|set| {
13966 let mut key: Vec<String> =
13967 set.iter().map(|e| alloc::format!("{e}")).collect();
13968 key.sort();
13969 if seen.contains(&key) {
13970 false
13971 } else {
13972 seen.push(key);
13973 true
13974 }
13975 });
13976 }
13977 if total.len() > 1 {
13978 let mut universe: Vec<Expr> = Vec::new();
13979 for set in &total {
13980 for k in set {
13981 if !universe.iter().any(|u| u == k) {
13982 universe.push(k.clone());
13983 }
13984 }
13985 }
13986 grouping_universe = universe;
13987 let primary = total[0].clone();
13988 grouping_sets = total;
13989 Some(primary)
13990 } else {
13991 // One set (a plain GROUP BY list, or a single-set
13992 // spelling like GROUPING SETS ((a, b))). An EMPTY
13993 // single set — GROUPING SETS (()) — stays
13994 // `Some(vec![])`: the grand-total group, which must
13995 // run the aggregate path.
13996 Some(total.into_iter().next().unwrap_or_default())
13997 }
13998 }
13999 } else {
14000 None
14001 };
14002 let having = if matches!(self.peek(), Token::Having) {
14003 self.advance();
14004 Some(self.parse_expr(0)?)
14005 } else {
14006 None
14007 };
14008 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14009 // OVER w parsed to a marker above; inline each definition
14010 // into the referencing WindowFunction nodes.
14011 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14012 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14013 self.advance();
14014 loop {
14015 let wname = self.expect_ident_like()?;
14016 if !matches!(self.peek(), Token::As) {
14017 return Err(self.err(format!(
14018 "expected AS after WINDOW {wname}, got {:?}",
14019 self.peek()
14020 )));
14021 }
14022 self.advance();
14023 // v7.39 (round 229) — PG rejects a redefinition outright.
14024 if window_defs
14025 .iter()
14026 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14027 {
14028 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14029 }
14030 let def = self.parse_over_clause()?;
14031 // A definition may itself copy an earlier one
14032 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14033 // so resolve it against the defs already in scope. Same
14034 // copy rules as an `OVER (w1 …)` in the select list.
14035 let mut probe = Expr::WindowFunction {
14036 name: String::new(),
14037 args: Vec::new(),
14038 partition_by: def.0,
14039 order_by: def.1,
14040 frame: def.2,
14041 null_treatment: crate::ast::NullTreatment::Respect,
14042 filter: None,
14043 };
14044 Self::substitute_named_windows(&mut probe, &window_defs)
14045 .map_err(|m| self.err(m))?;
14046 let Expr::WindowFunction {
14047 partition_by,
14048 order_by,
14049 frame,
14050 ..
14051 } = probe
14052 else {
14053 unreachable!("probe is a WindowFunction")
14054 };
14055 window_defs.push((wname, (partition_by, order_by, frame)));
14056 if matches!(self.peek(), Token::Comma) {
14057 self.advance();
14058 continue;
14059 }
14060 break;
14061 }
14062 }
14063 // v7.39 (round 705) — which definitions did anything reference?
14064 // The ones nothing did used to be dropped here, unexamined, so
14065 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14066 // definition whether referenced or not. Their key expressions ride
14067 // out on the statement for the engine to resolve.
14068 let mut window_refs: Vec<String> = Vec::new();
14069 if !window_defs.is_empty() {
14070 for it in &items {
14071 if let SelectItem::Expr { expr, .. } = it {
14072 Self::collect_named_window_refs(expr, &mut window_refs);
14073 }
14074 }
14075 }
14076 let window_check_exprs: Vec<Expr> = window_defs
14077 .iter()
14078 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14079 .flat_map(|(_, (partition, order, _))| {
14080 partition
14081 .iter()
14082 .cloned()
14083 .chain(order.iter().map(|(e, _, _)| e.clone()))
14084 })
14085 .collect();
14086 if !window_defs.is_empty()
14087 || items
14088 .iter()
14089 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14090 {
14091 for it in &mut items {
14092 if let SelectItem::Expr { expr, .. } = it {
14093 Self::substitute_named_windows(expr, &window_defs)
14094 .map_err(|m| self.err(m))?;
14095 }
14096 }
14097 }
14098 // `GROUP BY 1` — positional keys substitute with the Nth
14099 // select item's expression (same contract ORDER BY has had
14100 // since v6.x). Out-of-range positions error.
14101 let group_by = match group_by {
14102 Some(mut keys) => {
14103 for k in &mut keys {
14104 if let Expr::Literal(Literal::Integer(n)) = k {
14105 let idx = *n;
14106 if idx < 1 || idx as usize > items.len() {
14107 return Err(self.err(alloc::format!(
14108 "GROUP BY position {idx} is not in select list"
14109 )));
14110 }
14111 match &items[(idx - 1) as usize] {
14112 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14113 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14114 return Err(self.err(alloc::format!(
14115 "GROUP BY position {idx} references a wildcard item"
14116 )));
14117 }
14118 }
14119 }
14120 }
14121 Some(keys)
14122 }
14123 None => None,
14124 };
14125 let mut stmt = SelectStatement {
14126 locking: None,
14127 ctes: Vec::new(),
14128 distinct,
14129 distinct_on,
14130 items,
14131 from,
14132 where_,
14133 group_by,
14134 group_by_all,
14135 having,
14136 unions: Vec::new(),
14137 order_by: Vec::new(),
14138 limit: None,
14139 offset: None,
14140 limit_with_ties: false,
14141 window_check_exprs,
14142 };
14143 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14144 // first set is the primary (already on stmt.group_by); each
14145 // further set becomes a UNION ALL peer with its dropped
14146 // keys (universe minus the set) replaced by NULL literals
14147 // in the peer's items and group_by. PG-legal: non-grouped
14148 // select items must be group keys or aggregates, so a
14149 // dropped key's occurrences in the projection are exactly
14150 // the ones to nullify.
14151 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14152 // over a plain GROUP BY (every argument must be a group key; the
14153 // mask is then 0) and rejects anything else with 42803. SPG's
14154 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14155 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14156 // function `grouping`".
14157 if grouping_sets.len() <= 1 {
14158 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14159 let mut calls: Vec<Expr> = Vec::new();
14160 for item in &stmt.items {
14161 if let SelectItem::Expr { expr, .. } = item {
14162 Self::collect_grouping_calls(expr, &mut calls);
14163 }
14164 }
14165 if let Some(h) = &stmt.having {
14166 Self::collect_grouping_calls(h, &mut calls);
14167 }
14168 for call in &calls {
14169 let Expr::FunctionCall { args, .. } = call else {
14170 continue;
14171 };
14172 for a in args {
14173 if !keys.iter().any(|k| k == a) {
14174 return Err(self.err(
14175 "arguments to GROUPING must be grouping expressions of the associated query level"
14176 .to_string(),
14177 ));
14178 }
14179 }
14180 }
14181 if !calls.is_empty() {
14182 for item in &mut stmt.items {
14183 if let SelectItem::Expr { expr, .. } = item {
14184 Self::substitute_grouping_calls(expr, &[]);
14185 }
14186 }
14187 if let Some(h) = &mut stmt.having {
14188 Self::substitute_grouping_calls(h, &[]);
14189 }
14190 }
14191 }
14192 if grouping_sets.len() > 1 {
14193 // The primary set's own dropped keys nullify in the
14194 // HEAD's projection too (GROUPING SETS's first set may
14195 // omit keys other sets use).
14196 let primary = grouping_sets[0].clone();
14197 let head_dropped: Vec<Expr> = grouping_universe
14198 .iter()
14199 .filter(|u| !primary.iter().any(|k| k == *u))
14200 .cloned()
14201 .collect();
14202 for set in grouping_sets.iter().skip(1) {
14203 let mut peer = stmt.clone();
14204 peer.unions = Vec::new();
14205 let dropped: Vec<&Expr> = grouping_universe
14206 .iter()
14207 .filter(|u| !set.iter().any(|k| k == *u))
14208 .collect();
14209 // Empty set = grand-total group: `Some(vec![])` forces
14210 // the aggregate path (one collapsed row) instead of a
14211 // per-row passthrough. See the primary-set note above.
14212 peer.group_by = Some(set.clone());
14213 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14214 for item in &mut peer.items {
14215 if let SelectItem::Expr { expr, alias } = item {
14216 if dropped.iter().any(|d| *d == expr) {
14217 // v7.39 — keep the dropped key's name on the
14218 // NULL literal so the UNION output column
14219 // (and any top-level ORDER BY on it) still
14220 // resolves.
14221 if alias.is_none()
14222 && let Expr::Column(c) = &expr
14223 {
14224 *alias = Some(c.name.clone());
14225 }
14226 *expr = Expr::Literal(Literal::Null);
14227 } else {
14228 Self::substitute_grouping_calls(expr, &dropped_owned);
14229 }
14230 }
14231 }
14232 if let Some(h) = &mut peer.having {
14233 Self::substitute_grouping_calls(h, &dropped_owned);
14234 }
14235 stmt.unions.push((UnionKind::All, peer));
14236 }
14237 for item in &mut stmt.items {
14238 if let SelectItem::Expr { expr, alias } = item {
14239 if head_dropped.iter().any(|d| d == expr) {
14240 if alias.is_none()
14241 && let Expr::Column(c) = &expr
14242 {
14243 *alias = Some(c.name.clone());
14244 }
14245 *expr = Expr::Literal(Literal::Null);
14246 } else {
14247 Self::substitute_grouping_calls(expr, &head_dropped);
14248 }
14249 }
14250 }
14251 if let Some(h) = &mut stmt.having {
14252 Self::substitute_grouping_calls(h, &head_dropped);
14253 }
14254 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14255 // (while `grouping_universe` / the per-branch sets are in scope). For
14256 // each grouping() call in it, inject a per-branch hidden column
14257 // `__grp_ord_K` carrying that branch's mask into the head + every
14258 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14259 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14260 // from the final output. A standalone grouping-set query has ORDER BY
14261 // (not an explicit set-op) next, so consuming it here is safe.
14262 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14263 // rollup carries the hierarchical order: sort by the grouping
14264 // keys with the rolled-up NULLs last, which is exactly the
14265 // interleaving both oracles emit. A client's own ORDER BY wins,
14266 // which is what MySQL does (MariaDB refuses to let one be
14267 // written at all).
14268 // The synthesised keys have to travel the SAME path a written
14269 // ORDER BY does: the block below is what turns a `grouping()`
14270 // call into the per-branch `__grp_ord_K` column the engine can
14271 // actually sort on. Bypassing it left a bare `grouping(text)`
14272 // for the evaluator to reject.
14273 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14274 self.parse_order_by_keys()?
14275 } else if mysql_rollup {
14276 Self::mysql_rollup_order(&grouping_universe)
14277 } else {
14278 Vec::new()
14279 };
14280 if !synthesised_or_parsed.is_empty() {
14281 let mut order_keys = synthesised_or_parsed;
14282 let mut grp_exprs: Vec<Expr> = Vec::new();
14283 for ob in &order_keys {
14284 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14285 }
14286 for (k, gexpr) in grp_exprs.iter().enumerate() {
14287 let colname = alloc::format!("__grp_ord_{k}");
14288 // Head branch (primary set) uses `head_dropped`.
14289 let mut he = gexpr.clone();
14290 Self::substitute_grouping_calls(&mut he, &head_dropped);
14291 stmt.items.push(SelectItem::Expr {
14292 expr: he,
14293 alias: Some(colname.clone()),
14294 });
14295 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14296 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14297 let set = &grouping_sets[i + 1];
14298 let dropped: Vec<Expr> = grouping_universe
14299 .iter()
14300 .filter(|u| !set.iter().any(|k| k == *u))
14301 .cloned()
14302 .collect();
14303 let mut pe = gexpr.clone();
14304 Self::substitute_grouping_calls(&mut pe, &dropped);
14305 peer.items.push(SelectItem::Expr {
14306 expr: pe,
14307 alias: Some(colname.clone()),
14308 });
14309 }
14310 }
14311 for ob in &mut order_keys {
14312 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14313 }
14314 stmt.order_by = order_keys;
14315 }
14316 }
14317 Ok(stmt)
14318 }
14319
14320 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14321 /// as ORDER BY keys.
14322 ///
14323 /// Per key: the rollup marker, then the key. Sorting on the key alone
14324 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14325 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14326 /// the ROLLUP-introduced NULL last, and both print as NULL.
14327 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14328 /// real group including the data-NULL one, 1 only for the row the
14329 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14330 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14331 ///
14332 /// `#[inline(never)]`: its locals must not join the statement parser's
14333 /// frame, which round 430 measured sitting against the nesting guard.
14334 #[inline(never)]
14335 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14336 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14337 for e in keys {
14338 out.push(OrderBy {
14339 expr: Expr::FunctionCall {
14340 name: "grouping".into(),
14341 args: alloc::vec![e.clone()],
14342 },
14343 desc: false,
14344 nulls_first: None,
14345 collation: None,
14346 });
14347 out.push(OrderBy {
14348 expr: e.clone(),
14349 desc: false,
14350 // MySQL orders NULL first on an ascending key.
14351 nulls_first: Some(true),
14352 collation: None,
14353 });
14354 }
14355 out
14356 }
14357
14358 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14359 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14360 #[inline(never)]
14361 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14362 use crate::ast::MaintainKind;
14363 self.skip_paren_option_list();
14364 let kind = match self.peek() {
14365 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14366 Token::Table | Token::Index => {
14367 self.advance();
14368 MaintainKind::ReindexRelation
14369 }
14370 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14371 "index" | "table" => {
14372 self.advance();
14373 MaintainKind::ReindexRelation
14374 }
14375 "schema" => {
14376 self.advance();
14377 MaintainKind::ReindexSchema
14378 }
14379 "system" | "database" => {
14380 self.advance();
14381 MaintainKind::Whole
14382 }
14383 // PG requires the object type; anything else is the
14384 // caller's problem, not something to swallow.
14385 _ => MaintainKind::ReindexRelation,
14386 },
14387 _ => MaintainKind::Whole,
14388 };
14389 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14390 // allows the plain form, so the modifier is recorded rather than
14391 // skipped. It still has no effect on how the reindex runs.
14392 let mut concurrently = false;
14393 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14394 self.advance();
14395 concurrently = true;
14396 }
14397 let target = self.take_optional_maintain_name();
14398 self.consume_until_statement_boundary();
14399 Ok(Statement::Maintain {
14400 kind,
14401 concurrently,
14402 target,
14403 })
14404 }
14405
14406 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14407 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14408 #[inline(never)]
14409 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14410 use crate::ast::MaintainKind;
14411 self.skip_paren_option_list();
14412 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14413 self.advance();
14414 }
14415 let target = self.take_optional_maintain_name();
14416 self.consume_until_statement_boundary();
14417 Ok(Statement::Maintain {
14418 kind: if target.is_some() {
14419 MaintainKind::ClusterRelation
14420 } else {
14421 MaintainKind::Whole
14422 },
14423 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14424 // transaction block quite happily (measured).
14425 concurrently: false,
14426 target,
14427 })
14428 }
14429
14430 /// The next token as a relation / schema name, when there is one.
14431 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14432 match self.peek() {
14433 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14434 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14435 _ => None,
14436 },
14437 _ => None,
14438 }
14439 }
14440
14441 /// A parenthesised option list, absorbed.
14442 fn skip_paren_option_list(&mut self) {
14443 if !matches!(self.peek(), Token::LParen) {
14444 return;
14445 }
14446 let mut depth = 0usize;
14447 loop {
14448 match self.advance() {
14449 Token::LParen => depth += 1,
14450 Token::RParen => {
14451 depth -= 1;
14452 if depth == 0 {
14453 return;
14454 }
14455 }
14456 Token::Eof => return,
14457 _ => {}
14458 }
14459 }
14460 }
14461
14462 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14463 /// column list.
14464 ///
14465 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14466 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14467 /// / ALL. The three that describe physical storage have no meaning
14468 /// here, so they parse and change nothing rather than making a
14469 /// dump that mentions them fail to load.
14470 ///
14471 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14472 /// parse chain the nesting sentinel is tuned against.
14473 #[inline(never)]
14474 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14475 self.advance(); // LIKE
14476 let source = self.expect_ident_like()?;
14477 let mut options = crate::ast::LikeOptions::default();
14478 loop {
14479 let including = match self.peek() {
14480 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14481 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14482 _ => break,
14483 };
14484 self.advance();
14485 // `ALL` lexes as its own keyword, not an identifier.
14486 let opt = if matches!(self.peek(), Token::All) {
14487 self.advance();
14488 alloc::string::String::from("all")
14489 } else {
14490 self.expect_ident_like()?
14491 };
14492 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14493 o.defaults = on;
14494 o.constraints = on;
14495 o.identity = on;
14496 o.generated = on;
14497 o.indexes = on;
14498 o.comments = on;
14499 };
14500 match opt.to_ascii_lowercase().as_str() {
14501 "all" => set(&mut options, including),
14502 "defaults" => options.defaults = including,
14503 "constraints" => options.constraints = including,
14504 "identity" => options.identity = including,
14505 "generated" => options.generated = including,
14506 "indexes" => options.indexes = including,
14507 "comments" => options.comments = including,
14508 // No storage model to copy into.
14509 "storage" | "statistics" | "compression" => {}
14510 other => {
14511 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14512 }
14513 }
14514 }
14515 Ok(crate::ast::LikeSpec {
14516 source,
14517 at,
14518 options,
14519 })
14520 }
14521
14522 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14523 // Caller already consumed CREATE; we're sitting on TABLE.
14524 debug_assert!(matches!(self.peek(), Token::Table));
14525 self.advance();
14526 let if_not_exists = self.consume_if_not_exists();
14527 let name = self.expect_ident_like()?;
14528 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14529 // child shape has no column list; the child inherits its
14530 // columns from the parent at engine-DDL time. Detect it
14531 // before the `(` requirement below.
14532 if matches!(self.peek(), Token::Partition)
14533 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14534 {
14535 self.advance(); // PARTITION
14536 self.advance(); // of
14537 let partition_of = self.parse_partition_of_tail()?;
14538 return Ok(Statement::CreateTable(CreateTableStatement {
14539 temporary: false,
14540 name,
14541 engine: None,
14542 columns: Vec::new(),
14543 like_specs: Vec::new(),
14544 inherits: Vec::new(),
14545 if_not_exists,
14546 foreign_keys: Vec::new(),
14547 table_constraints: Vec::new(),
14548 partition_by: None,
14549 partition_of: Some(partition_of),
14550 }));
14551 }
14552 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14553 // the materialized-view materialisation path (run the SELECT, infer the
14554 // column types, create + populate the table) but marks the node so the
14555 // executor creates a plain table without a mat-view registry entry.
14556 if matches!(self.peek(), Token::As) {
14557 self.advance();
14558 let body_stmt = self.parse_select_stmt()?;
14559 let Statement::Select(body) = body_stmt else {
14560 return Err(self.err(format!(
14561 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14562 )));
14563 };
14564 let with_data = self.parse_optional_with_data(true)?;
14565 return Ok(Statement::CreateMaterializedView(
14566 crate::ast::CreateMaterializedViewStatement {
14567 temporary: false,
14568 name,
14569 if_not_exists,
14570 columns: Vec::new(),
14571 body,
14572 with_data,
14573 as_plain_table: true,
14574 },
14575 ));
14576 }
14577 if !matches!(self.peek(), Token::LParen) {
14578 return Err(self.err(format!(
14579 "expected '(' after table name, got {:?}",
14580 self.peek()
14581 )));
14582 }
14583 self.advance();
14584 let mut columns = Vec::new();
14585 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14586 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14587 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14588 loop {
14589 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14590 // column list. It is how a child that adds nothing of its own is
14591 // written, and this loop demanded at least one entry: `syntax
14592 // error at or near ")"`. The child takes the parent's columns,
14593 // which the INHERITS clause already arranges.
14594 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14595 self.advance();
14596 break;
14597 }
14598 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14599 // clauses from column definitions. Constraints start
14600 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14601 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14602 // a column.
14603 if self.peek_table_level_pk_start() {
14604 table_constraints.push(self.parse_table_level_primary_key()?);
14605 } else if matches!(self.peek(), Token::Like) {
14606 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14607 // <opt> ]*`. The source table's shape lives in the catalog,
14608 // so this records the clause and the engine expands it.
14609 like_specs.push(self.parse_create_table_like(columns.len())?);
14610 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14611 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14612 table_constraints.push(self.parse_table_level_exclude()?);
14613 } else if self.peek_table_level_unique_start() {
14614 table_constraints.push(self.parse_table_level_unique()?);
14615 } else if self.peek_table_level_check_start() {
14616 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14617 table_constraints.push(self.parse_table_level_check()?);
14618 } else if self.peek_mysql_inline_key_start() {
14619 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14620 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14621 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14622 // inside the column list. Skip name + paren list;
14623 // for UNIQUE KEY, register as a UC.
14624 if let Some(uc) = self.parse_mysql_inline_key()? {
14625 table_constraints.push(uc);
14626 }
14627 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14628 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14629 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14630 // CHECK is named, and the named-CONSTRAINT arm used
14631 // to accept FOREIGN KEY only. The name is accepted
14632 // and discarded — same handling as every other SPG
14633 // constraint name.
14634 self.advance(); // CONSTRAINT
14635 // v7.39 (read01 round 48) — the name is kept now: the schema
14636 // stores it, so DROP / RENAME CONSTRAINT can find it.
14637 let con_name = self.expect_ident_like()?;
14638 let mut tc = match kind {
14639 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14640 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14641 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14642 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14643 };
14644 match &mut tc {
14645 crate::ast::TableConstraint::Check { name, .. }
14646 | crate::ast::TableConstraint::Unique { name, .. }
14647 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14648 | crate::ast::TableConstraint::Exclude { name, .. } => {
14649 *name = Some(con_name);
14650 }
14651 _ => {}
14652 }
14653 table_constraints.push(tc);
14654 } else if self.peek_constraint_or_fk_start() {
14655 foreign_keys.push(self.parse_table_level_fk()?);
14656 } else {
14657 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14658 // v7.13.0 — fold inline UNIQUE / CHECK column
14659 // constraints into table-level entries so the
14660 // engine path stays uniform.
14661 if col.is_unique {
14662 table_constraints.push(crate::ast::TableConstraint::Unique {
14663 name: None,
14664 columns: alloc::vec![col.name.clone()],
14665 nulls_not_distinct: col.unique_nulls_not_distinct,
14666 deferrable: col.constraint_deferrable,
14667 initially_deferred: col.constraint_initially_deferred,
14668 });
14669 }
14670 if let Some(check_expr) = col.check.clone() {
14671 table_constraints.push(crate::ast::TableConstraint::Check {
14672 name: None,
14673 expr: check_expr,
14674 not_valid: false,
14675 });
14676 }
14677 columns.push(col);
14678 if let Some(fk) = col_level_fk {
14679 foreign_keys.push(fk);
14680 }
14681 }
14682 match self.peek() {
14683 Token::Comma => {
14684 self.advance();
14685 }
14686 Token::RParen => {
14687 self.advance();
14688 break;
14689 }
14690 other => {
14691 return Err(
14692 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14693 );
14694 }
14695 }
14696 }
14697 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14698 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14699 // nothing is written between the parentheses.
14700 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14701 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14702 // empty parentheses were a parse error in their own right — quite apart
14703 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14704 // SPG does not have (filed separately).
14705 let _ = &like_specs;
14706 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14707 // It sits between the column list and the MySQL table options,
14708 // and it was a syntax error until this round.
14709 let mut inherits: Vec<String> = Vec::new();
14710 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14711 if k.eq_ignore_ascii_case("inherits"))
14712 {
14713 self.advance();
14714 if !matches!(self.peek(), Token::LParen) {
14715 return Err(self.err(alloc::format!(
14716 "expected ( after INHERITS, got {:?}",
14717 self.peek()
14718 )));
14719 }
14720 self.advance();
14721 loop {
14722 inherits.push(self.expect_ident_like()?);
14723 if matches!(self.peek(), Token::Comma) {
14724 self.advance();
14725 continue;
14726 }
14727 break;
14728 }
14729 if !matches!(self.peek(), Token::RParen) {
14730 return Err(self.err(alloc::format!(
14731 "expected ) closing INHERITS, got {:?}",
14732 self.peek()
14733 )));
14734 }
14735 self.advance();
14736 }
14737 // v7.14.0 — consume MySQL/MariaDB table options after the
14738 // closing `)`. mysqldump emits things like
14739 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14740 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14741 // SPG accepts all forms as no-ops (each option is
14742 // `<ident> [=] <ident-or-string>` separated by whitespace).
14743 let engine = self.consume_mysql_table_options();
14744 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14745 // SPG has no per-table reloptions, so accept and ignore them so a
14746 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14747 self.consume_with_reloptions();
14748 // v7.37.6-B — declarative-partition-parent suffix
14749 // (`PARTITION BY RANGE (key_col)`) sits after the column
14750 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14751 // and locks the key column at one ident; the engine then
14752 // verifies the column type is TIMESTAMPTZ.
14753 let partition_by = if matches!(self.peek(), Token::Partition) {
14754 self.advance(); // PARTITION
14755 if !self.peek_is_by() {
14756 return Err(self.err(format!(
14757 "expected BY after PARTITION, got {:?}",
14758 self.peek()
14759 )));
14760 }
14761 self.advance();
14762 Some(self.parse_partition_by_tail()?)
14763 } else {
14764 None
14765 };
14766 Ok(Statement::CreateTable(CreateTableStatement {
14767 temporary: false,
14768 name,
14769 engine,
14770 columns,
14771 like_specs,
14772 inherits,
14773 if_not_exists,
14774 foreign_keys,
14775 table_constraints,
14776 partition_by,
14777 partition_of: None,
14778 }))
14779 }
14780
14781 /// v7.37.6-B — case-insensitive ident match helper for the
14782 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14783 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14784 /// didn't burn a global keyword slot for each (see the
14785 /// `Token::Partition` doc-comment in `lexer.rs`).
14786 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14787 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14788 }
14789
14790 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14791 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14792 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14793 use crate::ast::{PartitionBySpec, PartitionKindAst};
14794 let kind = match self.peek() {
14795 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14796 self.advance();
14797 PartitionKindAst::Range
14798 }
14799 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14800 self.advance();
14801 PartitionKindAst::List
14802 }
14803 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14804 self.advance();
14805 PartitionKindAst::Hash
14806 }
14807 other => {
14808 return Err(self.err(format!(
14809 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14810 )));
14811 }
14812 };
14813 if !matches!(self.peek(), Token::LParen) {
14814 return Err(self.err(format!(
14815 "expected '(' after PARTITION BY <strategy>, got {:?}",
14816 self.peek()
14817 )));
14818 }
14819 self.advance();
14820 let mut key_columns = Vec::new();
14821 loop {
14822 key_columns.push(self.expect_ident_like()?);
14823 match self.peek() {
14824 Token::Comma => {
14825 self.advance();
14826 }
14827 Token::RParen => {
14828 self.advance();
14829 break;
14830 }
14831 other => {
14832 return Err(self.err(format!(
14833 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14834 )));
14835 }
14836 }
14837 }
14838 if key_columns.is_empty() {
14839 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14840 }
14841 Ok(PartitionBySpec { kind, key_columns })
14842 }
14843
14844 /// v7.37.6-B — after `PARTITION OF`, expect
14845 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14846 /// or
14847 /// <parent> DEFAULT
14848 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14849 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14850 let parent_name = self.expect_ident_like()?;
14851 // v7.37.6-B rejects an explicit column list — the child
14852 // inherits from the parent. mailrs round-7 taught us that
14853 // CREATE TABLE-side schema reconciliation hides drift, so
14854 // we surface this as a parse error rather than silently
14855 // ignoring user columns.
14856 if matches!(self.peek(), Token::LParen) {
14857 return Err(self.err(
14858 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14859 at v7.37.6-B; the child inherits its columns from the parent"
14860 .to_string(),
14861 ));
14862 }
14863 let bounds = match self.peek() {
14864 Token::Default => {
14865 self.advance();
14866 PartitionOfBoundsAst::Default
14867 }
14868 Token::For => {
14869 self.advance();
14870 if !matches!(self.peek(), Token::Values) {
14871 return Err(
14872 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14873 );
14874 }
14875 self.advance();
14876 // WITH is not a reserved Token in the lexer — it lexes
14877 // as Token::Ident("with"). Disambiguate manually.
14878 let want_with = matches!(
14879 self.peek(),
14880 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14881 );
14882 if want_with {
14883 self.advance();
14884 if !matches!(self.peek(), Token::LParen) {
14885 return Err(self.err(format!(
14886 "expected '(' after FOR VALUES WITH, got {:?}",
14887 self.peek()
14888 )));
14889 }
14890 self.advance();
14891 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14892 loop {
14893 let key = self.expect_ident_like()?;
14894 let n = match self.peek().clone() {
14895 Token::Integer(v) if u32::try_from(v).is_ok() => {
14896 self.advance();
14897 v as u32
14898 }
14899 other => {
14900 return Err(self.err(format!(
14901 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14902 )));
14903 }
14904 };
14905 match key.to_ascii_uppercase().as_str() {
14906 "MODULUS" => modulus = Some(n),
14907 "REMAINDER" => remainder = Some(n),
14908 other => {
14909 return Err(self.err(format!(
14910 "FOR VALUES WITH: unknown key {other:?}; \
14911 expected MODULUS or REMAINDER"
14912 )));
14913 }
14914 }
14915 match self.peek() {
14916 Token::Comma => {
14917 self.advance();
14918 }
14919 Token::RParen => {
14920 self.advance();
14921 break;
14922 }
14923 other => {
14924 return Err(self.err(format!(
14925 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14926 )));
14927 }
14928 }
14929 }
14930 let modulus = modulus
14931 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14932 let remainder = remainder.ok_or_else(|| {
14933 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14934 })?;
14935 if modulus == 0 {
14936 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14937 }
14938 if remainder >= modulus {
14939 return Err(self.err(format!(
14940 "FOR VALUES WITH: REMAINDER ({remainder}) \
14941 must be < MODULUS ({modulus})"
14942 )));
14943 }
14944 PartitionOfBoundsAst::Hash { modulus, remainder }
14945 } else {
14946 match self.peek() {
14947 Token::From => {
14948 self.advance();
14949 let lower = Box::new(self.parse_partition_bound_expr()?);
14950 if !matches!(self.peek(), Token::To) {
14951 return Err(self.err(format!(
14952 "expected TO after FROM (...), got {:?}",
14953 self.peek()
14954 )));
14955 }
14956 self.advance();
14957 let upper = Box::new(self.parse_partition_bound_expr()?);
14958 PartitionOfBoundsAst::Range { lower, upper }
14959 }
14960 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14961 Token::In => {
14962 self.advance();
14963 if !matches!(self.peek(), Token::LParen) {
14964 return Err(self.err(format!(
14965 "expected '(' after FOR VALUES IN, got {:?}",
14966 self.peek()
14967 )));
14968 }
14969 self.advance();
14970 let mut values = Vec::new();
14971 loop {
14972 values.push(self.parse_expr(0)?);
14973 match self.peek() {
14974 Token::Comma => {
14975 self.advance();
14976 }
14977 Token::RParen => {
14978 self.advance();
14979 break;
14980 }
14981 other => {
14982 return Err(self.err(format!(
14983 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14984 )));
14985 }
14986 }
14987 }
14988 if values.is_empty() {
14989 return Err(self.err(
14990 "FOR VALUES IN requires at least one literal".to_string(),
14991 ));
14992 }
14993 PartitionOfBoundsAst::List { values }
14994 }
14995 other => {
14996 return Err(self.err(format!(
14997 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14998 )));
14999 }
15000 }
15001 }
15002 }
15003 other => {
15004 return Err(self.err(format!(
15005 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15006 )));
15007 }
15008 };
15009 Ok(PartitionOfSpec {
15010 parent_name,
15011 bounds,
15012 })
15013 }
15014
15015 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15016 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15017 /// markers (no-arg builtins) so the engine resolves them
15018 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15019 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15020 if !matches!(self.peek(), Token::LParen) {
15021 return Err(self.err(format!(
15022 "expected '(' before partition bound, got {:?}",
15023 self.peek()
15024 )));
15025 }
15026 self.advance();
15027 let expr = match self.peek() {
15028 Token::Ident(s) | Token::QuotedIdent(s)
15029 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15030 {
15031 let name = s.to_ascii_uppercase();
15032 self.advance();
15033 crate::ast::Expr::FunctionCall {
15034 name,
15035 args: Vec::new(),
15036 }
15037 }
15038 _ => self.parse_expr(0)?,
15039 };
15040 if !matches!(self.peek(), Token::RParen) {
15041 return Err(self.err(format!(
15042 "expected ')' after partition bound, got {:?}",
15043 self.peek()
15044 )));
15045 }
15046 self.advance();
15047 Ok(expr)
15048 }
15049
15050 /// v7.14.0 — true when the next tokens look like an inline
15051 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15052 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15053 /// — each followed by an optional name + `(...)`. Critical:
15054 /// a column NAMED `key` / `index` (PG accepts as ident) must
15055 /// NOT be mistaken for the KEY constraint shape. We disambig
15056 /// by requiring the keyword to be followed by either `(` or
15057 /// `<ident> (`.
15058 fn peek_mysql_inline_key_start(&self) -> bool {
15059 let cur = self.peek();
15060 // Shapes:
15061 // KEY (cols)
15062 // KEY name (cols)
15063 // INDEX (cols)
15064 // INDEX name (cols)
15065 // UNIQUE KEY [name] (cols)
15066 // UNIQUE INDEX [name] (cols)
15067 // FULLTEXT [KEY|INDEX] [name] (cols)
15068 // SPATIAL [KEY|INDEX] [name] (cols)
15069 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15070 // tokens at skip = the position AFTER the index-form
15071 // keywords (KEY/INDEX) have been consumed.
15072 match self.tokens.get(skip) {
15073 Some(Token::LParen) => true,
15074 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15075 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15076 }
15077 _ => false,
15078 }
15079 };
15080 // `INDEX` lexes as Token::Index (reserved), not as
15081 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15082 // start; the peek helper below handles either.
15083 let is_key_or_index_tok = |t: &Token| -> bool {
15084 matches!(t, Token::Index)
15085 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15086 };
15087 match cur {
15088 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15089 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15090 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15091 }
15092 Token::Ident(s)
15093 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15094 {
15095 let nxt = self.tokens.get(self.pos + 1);
15096 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15097 self.pos + 2
15098 } else {
15099 self.pos + 1
15100 };
15101 after_keyword_followed_by_paren_or_ident_paren(after_after)
15102 }
15103 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15104 let nxt = self.tokens.get(self.pos + 1);
15105 if !nxt.is_some_and(is_key_or_index_tok) {
15106 return false;
15107 }
15108 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15109 }
15110 _ => false,
15111 }
15112 }
15113
15114 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15115 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15116 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15117 /// returns Some(TableConstraint::Index) so the engine builds
15118 /// a real BTree index on the leading column (mysqldump
15119 /// `KEY idx_posts_author (author_id)` shape).
15120 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15121 /// (the storage layer has no matching AM).
15122 fn parse_mysql_inline_key(
15123 &mut self,
15124 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15125 // Detect UNIQUE prefix.
15126 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15127 {
15128 self.advance();
15129 true
15130 } else {
15131 false
15132 };
15133 // Consume FULLTEXT / SPATIAL prefix and record which one
15134 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15135 // dedicated TableConstraint variant so the engine can
15136 // build a tsvector-GIN; SPATIAL still has no matching
15137 // AM, so it falls back to accept-as-no-op.
15138 let mut is_fulltext = false;
15139 let mut is_spatial = false;
15140 if let Token::Ident(s) = self.peek().clone() {
15141 if s.eq_ignore_ascii_case("fulltext") {
15142 self.advance();
15143 is_fulltext = true;
15144 } else if s.eq_ignore_ascii_case("spatial") {
15145 self.advance();
15146 is_spatial = true;
15147 }
15148 }
15149 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15150 // (reserved); accept either token shape.
15151 match self.peek() {
15152 Token::Index => {
15153 self.advance();
15154 }
15155 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15156 self.advance();
15157 }
15158 other => {
15159 return Err(self.err(alloc::format!(
15160 "expected KEY/INDEX in inline index declaration, got {other:?}"
15161 )));
15162 }
15163 }
15164 // Optional index name (an ident before the `(`).
15165 // v7.15.0 — capture the name when present so the engine
15166 // builds the secondary index under the user's chosen
15167 // name (matches mysqldump's `KEY idx_x (col)` shape).
15168 let mut idx_name: Option<String> = None;
15169 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15170 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15171 {
15172 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15173 idx_name = Some(s);
15174 }
15175 }
15176 // Optional `USING BTREE` / `USING HASH` (MySQL).
15177 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15178 self.advance();
15179 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15180 self.advance();
15181 }
15182 }
15183 // Required column list `(col [, col]*)`.
15184 if !matches!(self.peek(), Token::LParen) {
15185 return Err(self.err(alloc::format!(
15186 "expected '(' in inline KEY/INDEX, got {:?}",
15187 self.peek()
15188 )));
15189 }
15190 self.advance();
15191 let mut cols: Vec<String> = Vec::new();
15192 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15193 self.advance();
15194 cols.push(s);
15195 // Skip optional `(length)` per-column prefix.
15196 if matches!(self.peek(), Token::LParen) {
15197 let mut depth = 1usize;
15198 self.advance();
15199 while depth > 0 {
15200 match self.peek() {
15201 Token::LParen => depth += 1,
15202 Token::RParen => depth -= 1,
15203 Token::Eof => break,
15204 _ => {}
15205 }
15206 self.advance();
15207 }
15208 }
15209 // Skip optional ASC / DESC.
15210 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15211 || matches!(self.peek(), Token::Asc | Token::Desc)
15212 {
15213 self.advance();
15214 }
15215 if matches!(self.peek(), Token::Comma) {
15216 self.advance();
15217 continue;
15218 }
15219 break;
15220 }
15221 if matches!(self.peek(), Token::RParen) {
15222 self.advance();
15223 }
15224 // Trailing options on the inline index — comment / etc.
15225 // Skip until comma or `)`.
15226 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15227 self.advance();
15228 }
15229 if cols.is_empty() {
15230 return Ok(None);
15231 }
15232 if is_unique {
15233 // Carry the captured idx_name on UNIQUE too so future
15234 // engine work can name the underlying BTree
15235 // accordingly; today the unique-constraint installer
15236 // synthesises the name itself, but Display round-trip
15237 // benefits from preserving it.
15238 Ok(Some(crate::ast::TableConstraint::Unique {
15239 name: idx_name,
15240 columns: cols,
15241 nulls_not_distinct: false,
15242 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15243 deferrable: false,
15244 initially_deferred: false,
15245 }))
15246 } else if is_fulltext {
15247 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15248 // routes through `TableConstraint::FulltextIndex`;
15249 // the engine builds a tsvector-GIN over each named
15250 // column so MATCH AGAINST gets a real inverted
15251 // index instead of a silently-dropped declaration.
15252 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15253 name: idx_name,
15254 columns: cols,
15255 }))
15256 } else if is_spatial {
15257 // SPG has no native SPATIAL AM. Accept-as-no-op
15258 // (declaration is parsed, but no index is built).
15259 Ok(None)
15260 } else {
15261 // v7.15.0 — plain KEY / INDEX builds a real BTree
15262 // secondary index.
15263 Ok(Some(crate::ast::TableConstraint::Index {
15264 name: idx_name,
15265 columns: cols,
15266 }))
15267 }
15268 }
15269
15270 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15271 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15272 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15273 /// (in any order, separated by whitespace).
15274 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15275 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15276 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15277 /// bare ident here, and only the parenthesised form is reloptions (so this
15278 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15279 fn consume_with_reloptions(&mut self) {
15280 let is_with = matches!(
15281 self.peek(),
15282 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15283 );
15284 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15285 return;
15286 }
15287 self.advance(); // WITH
15288 self.advance(); // (
15289 let mut depth = 1u32;
15290 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15291 match self.peek() {
15292 Token::LParen => depth += 1,
15293 Token::RParen => depth -= 1,
15294 _ => {}
15295 }
15296 self.advance();
15297 }
15298 }
15299
15300 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15301 /// dropped with everything else here. The rest of the MySQL table
15302 /// options genuinely have no meaning for SPG's storage; the engine
15303 /// name does, because MySQL REFUSES one it does not know and a dump
15304 /// with a typo in it should not quietly become a table.
15305 fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15306 let mut engine: Option<alloc::string::String> = None;
15307 loop {
15308 // Heuristic: a table option is an ident (or `DEFAULT`
15309 // reserved keyword) followed by `=` and an
15310 // ident / string / integer.
15311 let name_lc = match self.peek().clone() {
15312 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15313 Token::Default => alloc::string::String::from("default"),
15314 _ => break,
15315 };
15316 let known = matches!(
15317 name_lc.as_str(),
15318 "engine"
15319 | "default"
15320 | "charset"
15321 | "collate"
15322 | "auto_increment"
15323 | "row_format"
15324 | "comment"
15325 | "pack_keys"
15326 | "stats_persistent"
15327 | "stats_auto_recalc"
15328 | "stats_sample_pages"
15329 | "key_block_size"
15330 | "tablespace"
15331 | "min_rows"
15332 | "max_rows"
15333 | "checksum"
15334 | "delay_key_write"
15335 | "insert_method"
15336 | "data"
15337 | "index"
15338 | "encryption"
15339 | "compression"
15340 );
15341 if !known {
15342 break;
15343 }
15344 self.advance(); // option name
15345 // `DEFAULT` optional prefix is followed by `CHARSET` /
15346 // `COLLATE`; consume the next ident too.
15347 if name_lc == "default" {
15348 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15349 self.advance();
15350 }
15351 }
15352 if matches!(self.peek(), Token::Eq) {
15353 self.advance();
15354 }
15355 match self.peek().clone() {
15356 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15357 if name_lc == "engine" {
15358 // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15359 // engine it does not know and names it back
15360 // exactly: `Unknown storage engine 'NoSuchEng'`,
15361 // measured. The lexer folds a bare identifier, so
15362 // the message quoted a name the dump did not
15363 // contain, which is the one thing that message is
15364 // for. Guarded the same way the column spelling
15365 // is: the span runs to the next token, so what
15366 // comes back has to be the same word.
15367 let written = self
15368 .source_span(self.pos, self.pos)
15369 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15370 .filter(|raw| raw.eq_ignore_ascii_case(&v))
15371 .map(alloc::string::String::from);
15372 engine = Some(written.unwrap_or(v));
15373 }
15374 self.advance();
15375 }
15376 Token::Integer(_) => {
15377 self.advance();
15378 }
15379 _ => {}
15380 }
15381 }
15382 engine
15383 }
15384
15385 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15386 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15387 /// sure (otherwise a column literally named `primary` would
15388 /// be mistaken).
15389 fn peek_table_level_pk_start(&self) -> bool {
15390 let cur = self.peek();
15391 let nxt = self.tokens.get(self.pos + 1);
15392 let nxt2 = self.tokens.get(self.pos + 2);
15393 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15394 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15395 let is_lparen = matches!(nxt2, Some(Token::LParen));
15396 is_primary && is_key && is_lparen
15397 }
15398
15399 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15400 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15401 /// (mailrs round-5 G10).
15402 fn peek_table_level_unique_start(&self) -> bool {
15403 let cur = self.peek();
15404 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15405 if !is_unique {
15406 return false;
15407 }
15408 let n1 = self.tokens.get(self.pos + 1);
15409 // Plain `UNIQUE (…)`.
15410 if matches!(n1, Some(Token::LParen)) {
15411 return true;
15412 }
15413 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15414 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15415 if !is_nulls {
15416 return false;
15417 }
15418 let n2 = self.tokens.get(self.pos + 2);
15419 let n3 = self.tokens.get(self.pos + 3);
15420 let n4 = self.tokens.get(self.pos + 4);
15421 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15422 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15423 return true;
15424 }
15425 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15426 if matches!(n2, Some(Token::Not))
15427 && matches!(n3, Some(Token::Distinct))
15428 && matches!(n4, Some(Token::LParen))
15429 {
15430 return true;
15431 }
15432 false
15433 }
15434
15435 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15436 self.advance(); // PRIMARY
15437 self.advance(); // KEY
15438 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15439 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15440 // 621 consumed and dropped them (the storing half of F08).
15441 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15442 Ok(crate::ast::TableConstraint::PrimaryKey {
15443 name: None,
15444 columns,
15445 deferrable,
15446 initially_deferred,
15447 })
15448 }
15449
15450 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15451 self.advance(); // UNIQUE
15452 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15453 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15454 // is `NULLS DISTINCT` per the SQL standard.
15455 let mut nulls_not_distinct = false;
15456 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15457 let n1 = self.tokens.get(self.pos + 1);
15458 let n2 = self.tokens.get(self.pos + 2);
15459 let is_not = matches!(n1, Some(Token::Not));
15460 let is_distinct = matches!(n2, Some(Token::Distinct));
15461 if is_not && is_distinct {
15462 self.advance(); // NULLS
15463 self.advance(); // NOT
15464 self.advance(); // DISTINCT
15465 nulls_not_distinct = true;
15466 } else if matches!(n1, Some(Token::Distinct)) {
15467 self.advance(); // NULLS
15468 self.advance(); // DISTINCT
15469 }
15470 }
15471 let columns = self.parse_paren_ident_list("UNIQUE")?;
15472 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15473 Ok(crate::ast::TableConstraint::Unique {
15474 name: None,
15475 columns,
15476 nulls_not_distinct,
15477 deferrable,
15478 initially_deferred,
15479 })
15480 }
15481
15482 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15483 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15484 /// expression.
15485 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15486 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15487 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15488 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15489 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15490 /// commit: `NOT` starts no other suffix here, but reading both
15491 /// tokens before advancing keeps the caller's error message intact
15492 /// if someone writes `NOT NULL` by mistake.
15493 fn parse_not_valid_suffix(&mut self) -> bool {
15494 if !matches!(self.peek(), Token::Not) {
15495 return false;
15496 }
15497 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15498 {
15499 return false;
15500 }
15501 self.advance();
15502 self.advance();
15503 true
15504 }
15505
15506 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15507 self.advance(); // EXCLUDE
15508 // Optional `USING <method>`.
15509 let mut method = None;
15510 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15511 self.advance();
15512 method = Some(match self.advance() {
15513 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15514 other => {
15515 return Err(self.err(alloc::format!(
15516 "expected index method after USING, got {other:?}"
15517 )));
15518 }
15519 });
15520 }
15521 if !matches!(self.peek(), Token::LParen) {
15522 return Err(self.err(alloc::format!(
15523 "expected '(' after EXCLUDE, got {:?}",
15524 self.peek()
15525 )));
15526 }
15527 self.advance();
15528 let mut elements: Vec<(String, String)> = Vec::new();
15529 loop {
15530 let col = match self.advance() {
15531 Token::Ident(s) | Token::QuotedIdent(s) => s,
15532 other => {
15533 return Err(self.err(alloc::format!(
15534 "expected column name in EXCLUDE, got {other:?}"
15535 )));
15536 }
15537 };
15538 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15539 return Err(self.err(alloc::format!(
15540 "expected WITH after EXCLUDE column, got {:?}",
15541 self.peek()
15542 )));
15543 }
15544 self.advance();
15545 let op = match self.advance() {
15546 Token::InetOverlap => String::from("&&"),
15547 Token::Intersects => String::from("?#"),
15548 Token::IsBelow => String::from("<^"),
15549 Token::IsAbove => String::from(">^"),
15550 Token::PatternLt => String::from("~<~"),
15551 Token::PatternLtEq => String::from("~<=~"),
15552 Token::PatternGt => String::from("~>~"),
15553 Token::PatternGtEq => String::from("~>=~"),
15554 Token::TsMatchOld => String::from("@@@"),
15555 Token::Eq => String::from("="),
15556 Token::JsonContains => String::from("@>"),
15557 Token::JsonContainedBy => String::from("<@"),
15558 Token::OverLeft => String::from("&<"),
15559 Token::OverRight => String::from("&>"),
15560 other => {
15561 return Err(self.err(alloc::format!(
15562 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15563 )));
15564 }
15565 };
15566 elements.push((col, op));
15567 if matches!(self.peek(), Token::Comma) {
15568 self.advance();
15569 continue;
15570 }
15571 break;
15572 }
15573 if !matches!(self.peek(), Token::RParen) {
15574 return Err(self.err(alloc::format!(
15575 "expected ')' to close EXCLUDE, got {:?}",
15576 self.peek()
15577 )));
15578 }
15579 self.advance();
15580 Ok(crate::ast::TableConstraint::Exclude {
15581 name: None,
15582 method,
15583 elements,
15584 })
15585 }
15586
15587 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15588 self.advance(); // CHECK
15589 if !matches!(self.peek(), Token::LParen) {
15590 return Err(self.err(alloc::format!(
15591 "expected '(' after CHECK, got {:?}",
15592 self.peek()
15593 )));
15594 }
15595 self.advance();
15596 let expr = self.parse_expr(0)?;
15597 if !matches!(self.peek(), Token::RParen) {
15598 return Err(self.err(alloc::format!(
15599 "expected ')' to close CHECK predicate, got {:?}",
15600 self.peek()
15601 )));
15602 }
15603 self.advance();
15604 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15605 // are no existing rows for PG to skip, so it rejects the suffix.
15606 Ok(crate::ast::TableConstraint::Check {
15607 name: None,
15608 expr,
15609 not_valid: false,
15610 })
15611 }
15612
15613 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15614 fn peek_table_level_check_start(&self) -> bool {
15615 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15616 }
15617
15618 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15619 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15620 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15621 /// own CONSTRAINT prefix).
15622 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15623 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15624 return None;
15625 }
15626 // tokens[pos+1] is the constraint name (any ident-like);
15627 // tokens[pos+2] is the kind keyword.
15628 match self.tokens.get(self.pos + 2) {
15629 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15630 Some(NamedTableConstraintKind::Check)
15631 }
15632 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15633 Some(NamedTableConstraintKind::Unique)
15634 }
15635 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15636 Some(NamedTableConstraintKind::PrimaryKey)
15637 }
15638 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15639 Some(NamedTableConstraintKind::Exclude)
15640 }
15641 _ => None,
15642 }
15643 }
15644
15645 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15646 if !matches!(self.peek(), Token::LParen) {
15647 return Err(self.err(alloc::format!(
15648 "expected '(' after {ctx}, got {:?}",
15649 self.peek()
15650 )));
15651 }
15652 self.advance();
15653 let mut out = Vec::new();
15654 loop {
15655 out.push(self.expect_ident_like()?);
15656 match self.peek() {
15657 Token::Comma => {
15658 self.advance();
15659 }
15660 Token::RParen => {
15661 self.advance();
15662 break;
15663 }
15664 other => {
15665 return Err(self.err(alloc::format!(
15666 "expected ',' or ')' in {ctx} list, got {other:?}"
15667 )));
15668 }
15669 }
15670 }
15671 if out.is_empty() {
15672 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15673 }
15674 Ok(out)
15675 }
15676
15677 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15678 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15679 /// table-level FK; a column def never starts with either keyword
15680 /// (column names are not in this reserved set).
15681 fn peek_constraint_or_fk_start(&self) -> bool {
15682 let is_constraint_kw = matches!(
15683 self.peek(),
15684 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15685 );
15686 let is_foreign_kw = matches!(
15687 self.peek(),
15688 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15689 );
15690 is_constraint_kw || is_foreign_kw
15691 }
15692
15693 /// v7.6.0 — parse a table-level FK clause:
15694 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15695 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15696 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15697 let mut name: Option<String> = None;
15698 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15699 self.advance();
15700 name = Some(self.expect_ident_like()?);
15701 }
15702 // `FOREIGN`
15703 match self.advance() {
15704 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15705 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15706 }
15707 // `KEY`
15708 match self.advance() {
15709 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15710 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15711 }
15712 // `(col, col, ...)`
15713 if !matches!(self.peek(), Token::LParen) {
15714 return Err(self.err(format!(
15715 "expected '(' after FOREIGN KEY, got {:?}",
15716 self.peek()
15717 )));
15718 }
15719 self.advance();
15720 let mut columns = Vec::new();
15721 loop {
15722 columns.push(self.expect_ident_like()?);
15723 match self.peek() {
15724 Token::Comma => {
15725 self.advance();
15726 }
15727 Token::RParen => {
15728 self.advance();
15729 break;
15730 }
15731 other => {
15732 return Err(self.err(format!(
15733 "expected ',' or ')' in FK column list, got {other:?}"
15734 )));
15735 }
15736 }
15737 }
15738 if columns.is_empty() {
15739 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15740 }
15741 let (
15742 parent_table,
15743 parent_columns,
15744 on_delete,
15745 on_update,
15746 match_type,
15747 deferrable,
15748 initially_deferred,
15749 ) = self.parse_references_tail(columns.len())?;
15750 Ok(ForeignKeyConstraint {
15751 name,
15752 columns,
15753 parent_table,
15754 parent_columns,
15755 on_delete,
15756 on_update,
15757 match_type,
15758 deferrable,
15759 initially_deferred,
15760 })
15761 }
15762
15763 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15764 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15765 /// the local column count, used to default the parent column
15766 /// list when omitted (SQL spec: parent's PK is implied).
15767 fn parse_references_tail(
15768 &mut self,
15769 expected_arity: usize,
15770 ) -> Result<
15771 (
15772 String,
15773 Vec<String>,
15774 FkAction,
15775 FkAction,
15776 crate::ast::MatchType,
15777 // v7.39 (round 288) — deferrable, initially_deferred.
15778 bool,
15779 bool,
15780 ),
15781 ParseError,
15782 > {
15783 match self.advance() {
15784 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15785 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15786 }
15787 let parent_table = self.expect_ident_like()?;
15788 let mut parent_columns: Vec<String> = Vec::new();
15789 if matches!(self.peek(), Token::LParen) {
15790 self.advance();
15791 loop {
15792 parent_columns.push(self.expect_ident_like()?);
15793 match self.peek() {
15794 Token::Comma => {
15795 self.advance();
15796 }
15797 Token::RParen => {
15798 self.advance();
15799 break;
15800 }
15801 other => {
15802 return Err(self.err(format!(
15803 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15804 )));
15805 }
15806 }
15807 }
15808 }
15809 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15810 return Err(self.err(format!(
15811 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15812 expected_arity,
15813 parent_columns.len()
15814 )));
15815 }
15816 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15817 // it between the referenced column list and the ON / DEFERRABLE
15818 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15819 // is skipped when any referencing column is NULL), so SIMPLE —
15820 // the default, and the only spelling pg_dump emits — is accepted
15821 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15822 // mixed-NULL rule, which is not wired yet; reject them honestly
15823 // rather than silently applying SIMPLE (PG itself errors on
15824 // MATCH PARTIAL as "not yet implemented").
15825 let mut match_type = crate::ast::MatchType::Simple;
15826 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15827 self.advance();
15828 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15829 // SIMPLE / PARTIAL arrive as bare identifiers.
15830 let kind = match self.advance() {
15831 Token::Full => "FULL".to_string(),
15832 Token::Ident(s) => s.to_uppercase(),
15833 other => {
15834 return Err(self.err(format!(
15835 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15836 )));
15837 }
15838 };
15839 match kind.as_str() {
15840 "SIMPLE" => {} // Default — match_type stays Simple.
15841 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15842 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15843 "FULL" => match_type = crate::ast::MatchType::Full,
15844 "PARTIAL" => {
15845 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15846 }
15847 _ => {
15848 return Err(self.err(format!(
15849 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15850 )));
15851 }
15852 }
15853 }
15854 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15855 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15856 // <action>` / `ON UPDATE <action>` in either order. PG /
15857 // pg_dump emits the timing clause AFTER the ON clauses
15858 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15859 // but the SQL spec allows either order. We loop over
15860 // every possible trailer and dispatch on the next token,
15861 // stopping when nothing matches. Phase 3.1 changes the
15862 // bare DEFERRABLE form from hard-error to accept-as-
15863 // immediate; SPG is single-writer with no deferred-
15864 // constraint window so the runtime semantics are always
15865 // immediate even when INITIALLY DEFERRED is requested.
15866 // PG's default referential action (no ON DELETE / ON UPDATE
15867 // clause) is NO ACTION, not RESTRICT — the two enforce
15868 // identically in SPG (single-writer, no deferred window; see the
15869 // shared match arm in constraints.rs) but information_schema.
15870 // referential_constraints must report NO ACTION to match PG.
15871 let mut on_delete = FkAction::NoAction;
15872 let mut on_update = FkAction::NoAction;
15873 let mut seen_on_delete = false;
15874 let mut seen_on_update = false;
15875 let mut deferrable = false;
15876 let mut initially_deferred = false;
15877 loop {
15878 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15879 let before = self.pos;
15880 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15881 if self.pos != before {
15882 deferrable = d;
15883 initially_deferred = idef;
15884 continue;
15885 }
15886 // ON DELETE / ON UPDATE.
15887 if !matches!(self.peek(), Token::On) {
15888 break;
15889 }
15890 self.advance();
15891 let which = self.advance();
15892 let action = self.parse_fk_action()?;
15893 match which {
15894 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15895 if seen_on_delete {
15896 return Err(self.err("ON DELETE specified twice".into()));
15897 }
15898 seen_on_delete = true;
15899 on_delete = action;
15900 }
15901 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15902 if seen_on_update {
15903 return Err(self.err("ON UPDATE specified twice".into()));
15904 }
15905 seen_on_update = true;
15906 on_update = action;
15907 }
15908 other => {
15909 return Err(
15910 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15911 );
15912 }
15913 }
15914 }
15915 Ok((
15916 parent_table,
15917 parent_columns,
15918 on_delete,
15919 on_update,
15920 match_type,
15921 deferrable,
15922 initially_deferred,
15923 ))
15924 }
15925
15926 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15927 /// NO ACTION`.
15928 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15929 match self.advance() {
15930 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15931 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15932 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15933 Token::Null => Ok(FkAction::SetNull),
15934 Token::Default => Ok(FkAction::SetDefault),
15935 other => Err(self.err(format!(
15936 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15937 ))),
15938 },
15939 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15940 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15941 other => Err(self.err(format!(
15942 "expected ACTION after NO in FK action, got {other:?}"
15943 ))),
15944 },
15945 other => Err(self.err(format!(
15946 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15947 ))),
15948 }
15949 }
15950
15951 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15952 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15953 fn consume_if_not_exists(&mut self) -> bool {
15954 // `IF` arrives as a bare Ident (we don't reserve it because it
15955 // also appears mid-expression in PG, though we don't support
15956 // those forms yet).
15957 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15958 if !looks_like_if {
15959 return false;
15960 }
15961 // Peek one ahead before committing: only consume IF when it's
15962 // actually `IF NOT EXISTS`.
15963 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15964 return false;
15965 }
15966 if !matches!(
15967 self.tokens.get(self.pos + 2),
15968 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15969 ) {
15970 return false;
15971 }
15972 self.advance(); // IF
15973 self.advance(); // NOT
15974 self.advance(); // EXISTS
15975 true
15976 }
15977
15978 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15979 /// Consumes IF EXISTS as a pair; returns false otherwise
15980 /// without consuming any tokens.
15981 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15982 /// ENABLE/DISABLE/FORCE/NO FORCE.
15983 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15984 for kw in ["row", "level", "security"] {
15985 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15986 {
15987 return Err(self.err(alloc::format!(
15988 "expected {} in ROW LEVEL SECURITY, got {:?}",
15989 kw.to_ascii_uppercase(),
15990 self.peek()
15991 )));
15992 }
15993 self.advance();
15994 }
15995 Ok(())
15996 }
15997
15998 fn consume_if_exists(&mut self) -> bool {
15999 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16000 if !looks_like_if {
16001 return false;
16002 }
16003 if !matches!(
16004 self.tokens.get(self.pos + 1),
16005 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16006 ) {
16007 return false;
16008 }
16009 self.advance(); // IF
16010 self.advance(); // EXISTS
16011 true
16012 }
16013
16014 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16015 /// qualifiers after an index column ref. ASC / DESC are
16016 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16017 /// We accept and discard them since single-column BTree
16018 /// stores rows in natural key order today.
16019 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16020 /// ORDER BY key. Returns None when absent.
16021 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16022 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16023 return Ok(None);
16024 }
16025 self.advance();
16026 match self.advance() {
16027 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16028 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16029 other => Err(self.err(alloc::format!(
16030 "expected FIRST or LAST after NULLS, got {other:?}"
16031 ))),
16032 }
16033 }
16034
16035 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16036 /// rather than discarded.
16037 ///
16038 /// SPG's index does not scan in a direction — column ordering is
16039 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16040 /// reproduction of the DDL, and dropping the clause meant
16041 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16042 /// dump lost it, and a schema diff saw drift on every run.
16043 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16044 let mut order = crate::ast::IndexColumnOrder::default();
16045 loop {
16046 match self.peek() {
16047 Token::Asc => {
16048 self.advance();
16049 }
16050 Token::Desc => {
16051 order.descending = true;
16052 self.advance();
16053 }
16054 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16055 let look = self.tokens.get(self.pos + 1);
16056 if matches!(
16057 look,
16058 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16059 || k.eq_ignore_ascii_case("last")
16060 ) {
16061 self.advance();
16062 order.nulls_first = Some(matches!(
16063 self.advance(),
16064 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16065 ));
16066 } else {
16067 break;
16068 }
16069 }
16070 _ => break,
16071 }
16072 }
16073 order
16074 }
16075
16076 fn parse_create_index_stmt_after_create(
16077 &mut self,
16078 is_unique: bool,
16079 ) -> Result<Statement, ParseError> {
16080 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16081 debug_assert!(matches!(self.peek(), Token::Index));
16082 self.advance();
16083 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16084 // SPG's CREATE INDEX is synchronous end-to-end today (real
16085 // CONCURRENTLY variant with restartable scans queues with
16086 // v7.39 indexes epic), so the modifier has no runtime effect
16087 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16088 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16089 // VIEW CONCURRENTLY.
16090 let mut concurrently = false;
16091 if matches!(
16092 self.peek(),
16093 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16094 ) {
16095 self.advance();
16096 concurrently = true;
16097 }
16098 let if_not_exists = self.consume_if_not_exists();
16099 // v7.39 (read01 round 93) — the index name is optional (PG since
16100 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16101 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16102 // was given; leave it empty and the engine derives a PG-style
16103 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16104 let name = if matches!(self.peek(), Token::On) {
16105 String::new()
16106 } else {
16107 self.expect_ident_like()?
16108 };
16109 if !matches!(self.peek(), Token::On) {
16110 return Err(self.err(format!(
16111 "expected ON after CREATE INDEX <name>, got {:?}",
16112 self.peek()
16113 )));
16114 }
16115 self.advance();
16116 let table = self.expect_ident_like()?;
16117 // Optional `USING <method>` — only recognised method in v2.0 is
16118 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16119 // ident `using` (we don't promote it to a reserved keyword
16120 // because it isn't reserved anywhere else in our SQL surface).
16121 let mut method_name: Option<String> = None;
16122 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16123 self.advance();
16124 let m = self.expect_ident_like()?;
16125 method_name = Some(m.to_ascii_lowercase());
16126 match m.to_ascii_lowercase().as_str() {
16127 "hnsw" => IndexMethod::Hnsw,
16128 "btree" => IndexMethod::BTree,
16129 "brin" => IndexMethod::Brin,
16130 // v7.12.3 — real GIN inverted index over `tsvector`.
16131 // v7.9.26b's `USING gin` → BTree silent fallback is
16132 // gone; the engine validates that the indexed column
16133 // is `tsvector` at CREATE INDEX time.
16134 "gin" => IndexMethod::Gin,
16135 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16136 // `USING spgist` / `USING hash` for their built-in
16137 // AMs that SPG doesn't have a matching
16138 // implementation for; degrade to BTree on the
16139 // leading column so the schema loads + the index
16140 // catalogue stays consistent. Operator pays the
16141 // planner cost only for the queries that would have
16142 // used the specialised AM.
16143 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16144 // v7.11.3 — pgvector ships both `ivfflat` and
16145 // `hnsw`. Customers shouldn't have to choose
16146 // their on-disk index method based on what SPG
16147 // implements; accept `ivfflat` as a synonym for
16148 // `hnsw` so PG schemas using either method drop
16149 // in. The vector distance op (`<->` / `<#>` /
16150 // `<=>`) at query time still picks the metric.
16151 "ivfflat" => IndexMethod::Hnsw,
16152 other => {
16153 return Err(self.err(alloc::format!(
16154 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16155 )));
16156 }
16157 }
16158 } else {
16159 IndexMethod::BTree
16160 };
16161 if !matches!(self.peek(), Token::LParen) {
16162 return Err(self.err(format!(
16163 "expected '(' before indexed column, got {:?}",
16164 self.peek()
16165 )));
16166 }
16167 self.advance();
16168 // v6.8.2 — accept either a bare column ident (legacy) or
16169 // an expression `fn(col, …)` for expression indexes.
16170 // Distinguish by peeking the token *after* the current
16171 // ident: `ident )` is the legacy column-only path;
16172 // anything else triggers the Pratt expression parser.
16173 // (`advance()` uses `mem::replace` to nil out the current
16174 // slot, so we can't save+rewind cleanly — peek-ahead via
16175 // direct index avoids the mutation.)
16176 let mut opclass: Option<String> = None;
16177 let mut key_collation: Option<String> = None;
16178 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16179 // Single column with `)` immediately after — fast path.
16180 // v7.9.29 — also: bare column followed by `,` (the
16181 // multi-column form `(a, b, c)`). Without this branch
16182 // the leading ident gets pulled into `parse_expr`
16183 // which then sets `expression = Some(Column(a))` and
16184 // breaks Display round-trip on the multi-column shape.
16185 Token::Ident(s) | Token::QuotedIdent(s)
16186 if matches!(
16187 self.tokens.get(self.pos + 1),
16188 Some(Token::RParen | Token::Comma)
16189 ) =>
16190 {
16191 self.advance();
16192 (s, None)
16193 }
16194 // v7.9.22 — single column followed by a pgvector
16195 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16196 // v7.15.0 — capture the opclass instead of discarding
16197 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16198 // → real trigram-shingle GIN over a TEXT column).
16199 // Vector/HNSW opclasses still take their distance
16200 // metric from the query operator (`<->` / `<#>` /
16201 // `<=>`), so for those callers the opclass stays
16202 // informational.
16203 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16204 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16205 // the schema and dispatch on the bare opclass, the same
16206 // treatment table/type names get.
16207 Token::Ident(s) | Token::QuotedIdent(s)
16208 if matches!(
16209 self.tokens.get(self.pos + 1),
16210 Some(Token::Ident(_) | Token::QuotedIdent(_))
16211 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16212 && matches!(
16213 self.tokens.get(self.pos + 3),
16214 Some(Token::Ident(op) | Token::QuotedIdent(op))
16215 if is_vector_opclass_name(op)
16216 ) =>
16217 {
16218 self.advance(); // column name
16219 self.advance(); // schema qualifier
16220 self.advance(); // dot
16221 let op_tok = self.advance();
16222 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16223 opclass = Some(op.to_ascii_lowercase());
16224 }
16225 (s, None)
16226 }
16227 // r1038 — an operator class is recognised by its POSITION, not
16228 // by a list of names. It used to be `is_vector_opclass_name`,
16229 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16230 // sentori's migration wrote — was a syntax error while
16231 // `USING gin (doc)` parsed. Anything sitting between a column
16232 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16233 // two bare identifiers in a row are not valid there otherwise.
16234 Token::Ident(s) | Token::QuotedIdent(s)
16235 if matches!(
16236 self.tokens.get(self.pos + 1),
16237 Some(Token::Ident(op) | Token::QuotedIdent(op))
16238 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16239 self.tokens.get(self.pos + 2)
16240 )
16241 ) =>
16242 {
16243 self.advance(); // column name
16244 // Capture the opclass token, lower-cased for
16245 // case-insensitive engine dispatch.
16246 let op_tok = self.advance();
16247 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16248 opclass = Some(op.to_ascii_lowercase());
16249 }
16250 (s, None)
16251 }
16252 Token::Ident(_) | Token::QuotedIdent(_) => {
16253 // v7.39 (round 538) — an explicit COLLATE on the key,
16254 // read by LOOKAHEAD because `parse_expr` absorbs the
16255 // clause as a no-op (SPG orders text by bytes, which is
16256 // the C collation, so it changes nothing to honour). PG
16257 // still PRINTS it: an explicitly written `"C"` and the
16258 // collation a column inherits are different collation
16259 // OBJECTS even where they sort identically, which is why
16260 // `(a COLLATE "C")` shows on a C-collation database too.
16261 if matches!(
16262 self.tokens.get(self.pos + 1),
16263 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16264 ) {
16265 key_collation = match self.tokens.get(self.pos + 2) {
16266 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16267 Some(n.clone())
16268 }
16269 _ => None,
16270 };
16271 }
16272 // v7.39.2 — the clause is read by the LOOKAHEAD above and
16273 // belongs to the KEY, not to the expression. Since
16274 // `COLLATE` became a node, letting `parse_expr` build one
16275 // here put the collation in twice and the key deparsed as
16276 // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16277 // is the same idea and already exists, so this borrows it:
16278 // absorb into the side channel, and the key's own
16279 // lookahead is what carries it.
16280 // v7.39.2 — and the key can only CARRY the byte-order
16281 // spellings. Absorbing into the side channel accepts any
16282 // name, so suppressing the node here without this check
16283 // silently accepted `(name COLLATE "en_US")`, which SPG's
16284 // index cannot honour — a refusal that was doing real
16285 // work, removed by the suppression and put back here.
16286 if let Some(name) = &key_collation {
16287 let lc = name.to_ascii_lowercase();
16288 let byte_order = matches!(
16289 lc.as_str(),
16290 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16291 );
16292 let mysql_ok = self.mysql_dialect
16293 && (lc.ends_with("_ci")
16294 || lc.ends_with("_bin")
16295 || lc == "binary"
16296 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16297 if !byte_order && !mysql_ok {
16298 return Err(self.err(alloc::format!(
16299 "COLLATE {name:?} is not supported in this position: an index \
16300 key carries the byte-order spellings only. Declare it on the \
16301 column (`x text COLLATE {name:?}`) instead"
16302 )));
16303 }
16304 }
16305 let saved_key_ctx = self.in_order_by_key;
16306 self.in_order_by_key = true;
16307 let key_expr = self.parse_expr(0);
16308 self.in_order_by_key = saved_key_ctx;
16309 let key_expr = key_expr?;
16310 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16311 self.err("expression index key must reference at least one column".into())
16312 })?;
16313 (primary, Some(key_expr))
16314 }
16315 // v7.37.43-T4 — parenthesised expression index key
16316 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16317 // PG's CREATE INDEX requires the expression to be in
16318 // its own parens to disambiguate function calls from
16319 // column lists, so this `LParen` is the inner open-paren
16320 // of an expression key. parse_expr handles the recursive
16321 // descent and consumes the matching `RParen`.
16322 Token::LParen => {
16323 let key_expr = self.parse_expr(0)?;
16324 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16325 self.err("expression index key must reference at least one column".into())
16326 })?;
16327 (primary, Some(key_expr))
16328 }
16329 other => {
16330 return Err(self.err(format!(
16331 "expected column ident or expression, got {other:?}"
16332 )));
16333 }
16334 };
16335 // v7.9.14 — accept extra comma-separated columns inside
16336 // the index key parens (`CREATE INDEX … (a, b, c)`).
16337 // mailrs F2. Each extra column may carry an optional
16338 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16339 // — parsed and discarded; SPG doesn't honour direction
16340 // on a BTree index today (column ordering is intrinsic
16341 // to the storage). v7.10 will widen to genuine composite
16342 // index keys.
16343 let mut extra_columns: Vec<String> = Vec::new();
16344 // The leading column may also have ASC/DESC after it — and that
16345 // one is the column SPG indexes, so its clause is kept.
16346 let key_order = self.consume_optional_index_column_qualifiers();
16347 while matches!(self.peek(), Token::Comma) {
16348 self.advance();
16349 let extra = self.expect_ident_like()?;
16350 let _ = self.consume_optional_index_column_qualifiers();
16351 extra_columns.push(extra);
16352 }
16353 if !matches!(self.peek(), Token::RParen) {
16354 return Err(self.err(format!(
16355 "expected ')' after indexed column / expression, got {:?}",
16356 self.peek()
16357 )));
16358 }
16359 self.advance();
16360 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16361 // index-only-scan annotation. Bare ident (not a reserved
16362 // keyword) so we test by case-insensitive string match.
16363 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16364 {
16365 self.advance();
16366 if !matches!(self.peek(), Token::LParen) {
16367 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16368 }
16369 self.advance();
16370 let mut cols = Vec::new();
16371 loop {
16372 cols.push(self.expect_ident_like()?);
16373 match self.peek() {
16374 Token::Comma => {
16375 self.advance();
16376 }
16377 Token::RParen => {
16378 self.advance();
16379 break;
16380 }
16381 other => {
16382 return Err(self.err(format!(
16383 "expected ',' or ')' in INCLUDE list, got {other:?}"
16384 )));
16385 }
16386 }
16387 }
16388 cols
16389 } else {
16390 Vec::new()
16391 };
16392 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16393 // storage parameters. pgvector emits `WITH (lists = N)` for
16394 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16395 // SPG's HNSW picks its own parameters today (tunable via
16396 // env vars), so the WITH clause is informational and dropped.
16397 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16398 self.advance();
16399 if !matches!(self.peek(), Token::LParen) {
16400 return Err(self.err(format!(
16401 "expected '(' after WITH in CREATE INDEX, got {:?}",
16402 self.peek()
16403 )));
16404 }
16405 self.advance();
16406 loop {
16407 if matches!(self.peek(), Token::RParen) {
16408 self.advance();
16409 break;
16410 }
16411 // Drain `key = value` or bare `key` tokens.
16412 let _ = self.advance(); // key
16413 if matches!(self.peek(), Token::Eq) {
16414 self.advance();
16415 let _ = self.advance(); // value (int / string / ident)
16416 }
16417 match self.peek() {
16418 Token::Comma => {
16419 self.advance();
16420 }
16421 Token::RParen => {
16422 self.advance();
16423 break;
16424 }
16425 other => {
16426 return Err(self.err(format!(
16427 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16428 )));
16429 }
16430 }
16431 }
16432 }
16433 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16434 // which sits between the key list and the WHERE clause.
16435 let mut nulls_not_distinct = false;
16436 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16437 let n1 = self.tokens.get(self.pos + 1);
16438 let n2 = self.tokens.get(self.pos + 2);
16439 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16440 self.advance(); // NULLS
16441 self.advance(); // NOT
16442 self.advance(); // DISTINCT
16443 nulls_not_distinct = true;
16444 } else if matches!(n1, Some(Token::Distinct)) {
16445 self.advance(); // NULLS
16446 self.advance(); // DISTINCT
16447 }
16448 }
16449 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16450 let partial_predicate = if matches!(self.peek(), Token::Where) {
16451 self.advance();
16452 Some(self.parse_expr(0)?)
16453 } else {
16454 None
16455 };
16456 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16457 // sense: uniqueness over an ANN structure has no clean
16458 // semantics. Reject early. (BRIN UNIQUE is similarly
16459 // meaningless — block both.)
16460 if is_unique && !matches!(method, IndexMethod::BTree) {
16461 return Err(self.err(alloc::format!(
16462 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16463 method
16464 )));
16465 }
16466 Ok(Statement::CreateIndex(CreateIndexStatement {
16467 concurrently,
16468 name,
16469 key_order,
16470 key_collation,
16471 table,
16472 column,
16473 nulls_not_distinct,
16474 method,
16475 if_not_exists,
16476 included_columns,
16477 partial_predicate,
16478 extra_columns: extra_columns.clone(),
16479 expression,
16480 is_unique,
16481 opclass,
16482 method_name,
16483 }))
16484 }
16485
16486 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16487 /// column-level `REFERENCES ...` clause. The trailing FK is
16488 /// normalised into table-level shape (single-element columns +
16489 /// parent_columns) so the engine sees one uniform constraint list.
16490 fn parse_column_def_with_fk(
16491 &mut self,
16492 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16493 let col = self.parse_column_def()?;
16494 // v7.39 (round 308, V29) — an explicitly named inline FK:
16495 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16496 // loop leaves this spelling intact precisely so the name can be
16497 // kept here; PG reports it in violation messages and matches it
16498 // in `SET CONSTRAINTS`.
16499 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16500 {
16501 self.advance();
16502 Some(self.expect_ident_like()?)
16503 } else {
16504 None
16505 };
16506 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16507 let inline_references = matches!(
16508 self.peek(),
16509 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16510 );
16511 if !inline_references {
16512 return Ok((col, None));
16513 }
16514 let (
16515 parent_table,
16516 parent_columns,
16517 on_delete,
16518 on_update,
16519 match_type,
16520 deferrable,
16521 initially_deferred,
16522 ) = self.parse_references_tail(1)?;
16523 let fk = ForeignKeyConstraint {
16524 name: declared_name,
16525 columns: vec![col.name.clone()],
16526 parent_table,
16527 parent_columns,
16528 on_delete,
16529 on_update,
16530 match_type,
16531 deferrable,
16532 initially_deferred,
16533 };
16534 Ok((col, Some(fk)))
16535 }
16536
16537 /// v7.13.0 — parse a column type (consuming the type ident and
16538 /// any trailing parameters / `[]`), without surrounding column
16539 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16540 /// Returns the resolved `ColumnTypeName` plus implied
16541 /// `(auto_increment, not_null)` flags from PG SERIAL family
16542 /// shorthands — callers that don't expect those (ALTER COLUMN
16543 /// TYPE) can discard them.
16544 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16545 let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16546 Ok(ty)
16547 }
16548
16549 #[allow(clippy::type_complexity)]
16550 fn parse_type_with_implied_flags(
16551 &mut self,
16552 ) -> Result<
16553 (
16554 ColumnTypeName,
16555 bool,
16556 bool,
16557 Option<String>,
16558 Collation,
16559 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16560 bool,
16561 // v7.39 (round 676) — the collation NAME as written, which the
16562 // `Collation` enum above cannot carry.
16563 Option<String>,
16564 bool,
16565 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16566 // list captured at type-parse time. None for all
16567 // non-ENUM types.
16568 Option<Vec<String>>,
16569 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16570 // list. Distinct from ENUM (subset semantics).
16571 Option<Vec<String>>,
16572 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16573 // width, lost when the type collapses to SmallInt / Int.
16574 Option<MysqlIntWidth>,
16575 // v7.39 (round 424) — declared fractional-seconds precision of a
16576 // MySQL temporal column (bare spelling = 0). None under PG.
16577 Option<u8>,
16578 // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16579 // two are different types on MySQL and SPG stores both as
16580 // `Timestamp`, so the spelling has to travel separately or
16581 // a dump silently rewrites the column.
16582 bool,
16583 // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16584 // display hint: it rounds on write.
16585 Option<(u8, u8)>,
16586 ),
16587 ParseError,
16588 > {
16589 let mut ty_ident = match self.advance() {
16590 Token::Ident(s) => s,
16591 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16592 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16593 // '<span>'` literal grammar. As a column type it lands
16594 // here directly; downstream resolution still uses the
16595 // canonical lowercase string.
16596 Token::Interval => "interval".to_string(),
16597 other => {
16598 return Err(ParseError {
16599 message: format!("expected column type, got {other:?}"),
16600 token_pos: self.consumed_pos(),
16601 });
16602 }
16603 };
16604 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16605 // pg_dump qualifies extension types (`public.vector(1024)`).
16606 // SPG is single-namespace; drop the schema and resolve the
16607 // bare type — same treatment table names already get.
16608 while matches!(self.peek(), Token::Dot) {
16609 self.advance();
16610 ty_ident = self.expect_ident_like()?;
16611 }
16612 let mut implied_auto_increment = false;
16613 let mut implied_not_null = false;
16614 let mut user_type_ref: Option<String> = None;
16615 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16616 // value list, captured here and bubbled up through the
16617 // ColumnDef so the engine can attach it to the column
16618 // schema (and validate INSERT cells against it).
16619 let mut inline_enum_variants: Option<Vec<String>> = None;
16620 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16621 let mut inline_set_variants: Option<Vec<String>> = None;
16622 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16623 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16624 // collapses to SmallInt / Int. Only under the MySQL dialect.
16625 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16626 // v7.39 (round 424) — the declared fractional-seconds precision of a
16627 // MySQL temporal column. Set by the temporal arms below; stays None
16628 // for PG (whose temporal columns keep full microseconds).
16629 let mut mysql_fsp: Option<u8> = None;
16630 let mut mysql_declared_timestamp = false;
16631 let mut mysql_float_md: Option<(u8, u8)> = None;
16632 let mut ty = match ty_ident.as_str() {
16633 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16634 "smallserial" | "serial2" => {
16635 implied_auto_increment = true;
16636 implied_not_null = true;
16637 ColumnTypeName::SmallInt
16638 }
16639 "serial" | "serial4" => {
16640 implied_auto_increment = true;
16641 implied_not_null = true;
16642 ColumnTypeName::Int
16643 }
16644 "bigserial" | "serial8" => {
16645 implied_auto_increment = true;
16646 implied_not_null = true;
16647 ColumnTypeName::BigInt
16648 }
16649 // MySQL flavours we accept by aliasing to the closest SPG
16650 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16651 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16652 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16653 // without semantic effect.
16654 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16655 // PG's internal type names; pg_dump and hand-written PG schemas
16656 // use them interchangeably with smallint / int / bigint (the cast
16657 // path already accepted them, only the column grammar didn't).
16658 "smallint" | "int2" => {
16659 // v7.14.0 — MySQL display-width on integers
16660 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16661 // parenthesised number is purely cosmetic — it
16662 // doesn't change storage. Accept + discard.
16663 self.consume_optional_paren_size();
16664 ColumnTypeName::SmallInt
16665 }
16666 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16667 // canonical encoding for BOOLEAN. Every MySQL driver
16668 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16669 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16670 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16671 // gave the customer i16-shaped values where the app
16672 // expected bool — a Tier-A silent type drift on
16673 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16674 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16675 // stay SmallInt (the legacy width-agnostic path).
16676 "tinyint" => {
16677 let width = self.peek_optional_paren_size_value();
16678 self.consume_optional_paren_size();
16679 if width == Some(1) {
16680 ColumnTypeName::Bool
16681 } else {
16682 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16683 // lost width so the write path can enforce -128..127.
16684 if self.mysql_dialect {
16685 mysql_int_width = Some(MysqlIntWidth::Tiny);
16686 }
16687 ColumnTypeName::SmallInt
16688 }
16689 }
16690 "mediumint" => {
16691 self.consume_optional_paren_size();
16692 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16693 if self.mysql_dialect {
16694 mysql_int_width = Some(MysqlIntWidth::Medium);
16695 }
16696 ColumnTypeName::Int
16697 }
16698 "int" | "integer" | "int4" => {
16699 self.consume_optional_paren_size();
16700 ColumnTypeName::Int
16701 }
16702 "bigint" | "int8" => {
16703 self.consume_optional_paren_size();
16704 ColumnTypeName::BigInt
16705 }
16706 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16707 // (mailrs round-5 G6). Consume the optional `PRECISION`
16708 // tail when the type keyword was `double` / `DOUBLE`.
16709 //
16710 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16711 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16712 // p in 1..=24 is real, 25..=53 is double precision, and
16713 // anything else is an error.
16714 "float" | "double" | "real" => {
16715 if ty_ident.eq_ignore_ascii_case("double")
16716 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16717 {
16718 self.advance();
16719 }
16720 if ty_ident.eq_ignore_ascii_case("real") {
16721 // v7.39 (round 274) — the two dialects genuinely
16722 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16723 // synonym for DOUBLE (8-byte). Round 269 made REAL
16724 // 32-bit globally and thereby narrowed the stored
16725 // precision of every MySQL REAL column.
16726 if self.mysql_dialect {
16727 ColumnTypeName::Float
16728 } else {
16729 ColumnTypeName::Real
16730 }
16731 } else if self.mysql_dialect
16732 && matches!(self.peek(), Token::LParen)
16733 && self.peek_paren_has_comma()
16734 {
16735 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16736 // display form (`FLOAT(10,2)`), which PG has no
16737 // equivalent of. It was `syntax error at or near ","`,
16738 // so the whole CREATE failed.
16739 //
16740 // v7.39.2 — the guard said `float` while the comment
16741 // said both, so `DOUBLE(10,2)` — which every legacy
16742 // MySQL schema uses for money — still failed the
16743 // whole CREATE with `syntax error at or near "("`.
16744 // Measured on 9.7.2: both forms are accepted, and the
16745 // digits are NOT a display hint, they round on write
16746 // (3.14159265358979 into either stores 3.14). The
16747 // rounding is recorded as a residual; accepting the
16748 // syntax and keeping the width is the half this
16749 // change makes.
16750 // v7.39.3 — keep the pair. The digits are not a
16751 // display hint: MySQL 9.7.2 ROUNDS on write and
16752 // refuses a value wider than `m` (errno 1264), so a
16753 // column declared for money held more precision here
16754 // than its schema said.
16755 let (m, d) = self.parse_optional_numeric_params()?;
16756 mysql_float_md = Some((
16757 u8::try_from(m).unwrap_or(u8::MAX),
16758 u8::try_from(d.max(0)).unwrap_or(u8::MAX),
16759 ));
16760 if ty_ident.eq_ignore_ascii_case("float") {
16761 ColumnTypeName::Real
16762 } else {
16763 ColumnTypeName::Float
16764 }
16765 } else if ty_ident.eq_ignore_ascii_case("float")
16766 && matches!(self.peek(), Token::LParen)
16767 {
16768 // PG words the two bounds differently, and
16769 // parse_paren_size already rejects a zero.
16770 let p = self.parse_paren_size("FLOAT")?;
16771 if p > 53 {
16772 return Err(self.err(String::from(
16773 "precision for type float must be less than 54 bits",
16774 )));
16775 }
16776 if p <= 24 {
16777 ColumnTypeName::Real
16778 } else {
16779 ColumnTypeName::Float
16780 }
16781 } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
16782 // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
16783 // eight (it is `float8`'s spelling there). SPG used
16784 // PG's for both, so a MySQL FLOAT column silently
16785 // kept more precision than MySQL does — measured,
16786 // 3.14159265358979 comes back as 3.14159 there and
16787 // came back whole here — and reported itself as
16788 // `double` to every reflection.
16789 //
16790 // This is the mirror of the REAL split above: the
16791 // two dialects disagree about which spelling means
16792 // which width, and one of them was already honoured.
16793 ColumnTypeName::Real
16794 } else {
16795 ColumnTypeName::Float
16796 }
16797 }
16798 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16799 "float4" => ColumnTypeName::Real,
16800 "float8" => ColumnTypeName::Float,
16801 "text" => ColumnTypeName::Text,
16802 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16803 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16804 // real MySQL schema and NONE of them existed: the CREATE
16805 // failed outright with `type "blob" does not exist`, so the
16806 // table was never made. The sizes differ only in MySQL's
16807 // maximum length, which SPG does not cap, so they collapse
16808 // onto TEXT and BYTEA the way the unsized spellings do.
16809 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16810 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16811 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16812 // enforce, consumed so the declaration parses.
16813 "varbinary" | "binary" => {
16814 self.consume_optional_paren_size();
16815 ColumnTypeName::Bytes
16816 }
16817 "name" => ColumnTypeName::Name,
16818 "xid" => ColumnTypeName::Xid,
16819 "oid" => ColumnTypeName::Oid,
16820 "xid8" => ColumnTypeName::Xid8,
16821 "bool" | "boolean" => ColumnTypeName::Bool,
16822 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16823 // an unbounded `character varying`, which the arm below has always
16824 // read as text. Only the short spelling demanded a length, so
16825 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16826 // there is — failed on `VARCHAR type requires (N)` while the long
16827 // spelling of the same thing was accepted. The same asymmetry
16828 // round 613 closed on the CAST side, here on the DDL side.
16829 "varchar" => {
16830 if matches!(self.peek(), Token::LParen) {
16831 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16832 } else {
16833 ColumnTypeName::Text
16834 }
16835 }
16836 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16837 // `character` below (SQL standard).
16838 "char" => {
16839 if matches!(self.peek(), Token::LParen) {
16840 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16841 } else {
16842 ColumnTypeName::Char(1)
16843 }
16844 }
16845 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16846 // `character(n)` = char, bare `character` = char(1). Unbounded
16847 // `character varying` maps to text.
16848 "character" => {
16849 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16850 self.advance();
16851 if matches!(self.peek(), Token::LParen) {
16852 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16853 } else {
16854 ColumnTypeName::Text
16855 }
16856 } else if matches!(self.peek(), Token::LParen) {
16857 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16858 } else {
16859 ColumnTypeName::Char(1)
16860 }
16861 }
16862 "vector" => {
16863 let dim = self.parse_paren_size("VECTOR")?;
16864 let encoding = self.parse_optional_vector_encoding()?;
16865 ColumnTypeName::Vector { dim, encoding }
16866 }
16867 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16868 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16869 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16870 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16871 // DECIMAL(10,2))` — how nearly every money column is written,
16872 // in either dialect — was a syntax error and the table was
16873 // never created. `FIXED` is MySQL's alias alone, so it is
16874 // taken only in that dialect.
16875 "numeric" | "decimal" | "dec" => {
16876 let (precision, scale) = self.parse_optional_numeric_params()?;
16877 ColumnTypeName::Numeric(precision, scale)
16878 }
16879 "fixed" if self.mysql_dialect => {
16880 let (precision, scale) = self.parse_optional_numeric_params()?;
16881 ColumnTypeName::Numeric(precision, scale)
16882 }
16883 "date" => ColumnTypeName::Date,
16884 // MySQL's `DATETIME` is the same domain as standard
16885 // `TIMESTAMP` — accept both spellings.
16886 "timestamp" | "datetime" => {
16887 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16888 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16889 // TIME ZONE` clause, so consume it first.
16890 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16891 // (it truncates on write and pads on render), so capture it;
16892 // a bare spelling means precision 0 there. PG stores µs always
16893 // and keeps `None`.
16894 let n = self.take_optional_paren_size();
16895 if self.mysql_dialect {
16896 mysql_fsp = Some(n.unwrap_or(0).min(6));
16897 // v7.39.2 — remember WHICH spelling was written.
16898 // MySQL and MariaDB keep `timestamp` and `datetime`
16899 // apart everywhere a client can read the type back,
16900 // and SPG reported `datetime` for both — so a dump
16901 // and reload silently changed the column's declared
16902 // type, and MySQL's TIMESTAMP is not DATETIME (a
16903 // different range, and UTC conversion on the way in
16904 // and out).
16905 mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
16906 }
16907 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16908 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16909 // the full form. SPG canonicalises:
16910 // - WITH TIME ZONE → Timestamptz
16911 // - WITHOUT TIME ZONE → Timestamp
16912 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16913 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16914 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16915 {
16916 self.advance(); // WITH
16917 self.advance(); // TIME
16918 self.advance(); // ZONE
16919 ColumnTypeName::Timestamptz
16920 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16921 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16922 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16923 {
16924 self.advance(); // WITHOUT
16925 self.advance(); // TIME
16926 self.advance(); // ZONE
16927 ColumnTypeName::Timestamp
16928 } else {
16929 // A second `(precision)` cannot legally follow, but the
16930 // old grammar tolerated it; keep that tolerance.
16931 self.consume_optional_paren_size();
16932 ColumnTypeName::Timestamp
16933 }
16934 }
16935 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16936 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16937 // only PG-wire OID differs.
16938 "timestamptz" => {
16939 self.consume_optional_paren_size();
16940 ColumnTypeName::Timestamptz
16941 }
16942 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16943 // validation. We accept the JSONB spelling too because
16944 // most PG clients default to it; SPG doesn't distinguish
16945 // the two (no path-operator perf advantage to model).
16946 "json" => ColumnTypeName::Json,
16947 "jsonb" => ColumnTypeName::Jsonb,
16948 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16949 // surface here. Same storage shape; mapping happens at
16950 // the engine side via the ColumnTypeName → DataType
16951 // resolver. Literal forms are handled at coerce_value
16952 // time so the lexer stays untouched.
16953 "bytea" | "bytes" => ColumnTypeName::Bytes,
16954 // v7.17.0 Phase 7 — PG network address types
16955 // v7.17.0 had a Text-backed fallback here for
16956 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16957 // each to a first-class type; the keywords are
16958 // bound below in the ζ-A block.
16959 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16960 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16961 // arrives in v7.12.1+; the type itself loads here so
16962 // mailrs's `scripts/init-schema.sql` runs unmodified.
16963 "tsvector" => ColumnTypeName::TsVector,
16964 "tsquery" => ColumnTypeName::TsQuery,
16965 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16966 // surface for Django / Rails / Hibernate's default
16967 // PK pattern.
16968 "uuid" => ColumnTypeName::Uuid,
16969 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16970 // Storage = three-field {months, days, micros}, catalog
16971 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16972 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16973 "interval" => {
16974 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16975 // SECOND` and an optional `(p)` precision. SPG stores the full
16976 // {months,days,micros}; consume + ignore the qualifier/precision.
16977 while matches!(self.peek(), Token::To)
16978 || matches!(self.peek(), Token::Ident(s) if matches!(
16979 s.to_ascii_lowercase().as_str(),
16980 "year" | "month" | "day" | "hour" | "minute" | "second"
16981 ))
16982 {
16983 self.advance();
16984 }
16985 self.consume_optional_paren_size();
16986 ColumnTypeName::Interval
16987 }
16988 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16989 // i64 microseconds since 00:00:00. Wire OID 1083.
16990 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16991 "time" => {
16992 // v7.39 (round 424) — MySQL TIME carries a semantic
16993 // fractional-seconds precision, bare meaning 0.
16994 let n = self.take_optional_paren_size();
16995 if self.mysql_dialect {
16996 mysql_fsp = Some(n.unwrap_or(0).min(6));
16997 }
16998 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16999 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17000 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17001 {
17002 self.advance();
17003 self.advance();
17004 self.advance();
17005 ColumnTypeName::TimeTz
17006 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17007 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17008 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17009 {
17010 self.advance();
17011 self.advance();
17012 self.advance();
17013 ColumnTypeName::Time
17014 } else {
17015 ColumnTypeName::Time
17016 }
17017 }
17018 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17019 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17020 "year" => ColumnTypeName::Year,
17021 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17022 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17023 "timetz" => ColumnTypeName::TimeTz,
17024 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17025 // Wire OID 790.
17026 "money" => ColumnTypeName::Money,
17027 // v7.17.0 Phase 3.P0-38 — PG range types.
17028 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17029 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17030 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17031 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17032 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17033 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17034 // v7.37.5 δ — PG 14+ multirange keywords.
17035 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17036 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17037 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17038 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17039 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17040 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17041 // v7.37.5 ε — PG geometry scalar keywords.
17042 "point" => ColumnTypeName::Point,
17043 "lseg" => ColumnTypeName::Lseg,
17044 "path" => ColumnTypeName::Path,
17045 "box" => ColumnTypeName::PgBox,
17046 "polygon" => ColumnTypeName::Polygon,
17047 "line" => ColumnTypeName::Line,
17048 "circle" => ColumnTypeName::Circle,
17049 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17050 "inet" => ColumnTypeName::Inet,
17051 "cidr" => ColumnTypeName::Cidr,
17052 "macaddr" => ColumnTypeName::Macaddr,
17053 "macaddr8" => ColumnTypeName::Macaddr8,
17054 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17055 // width in the value, so the optional `(N)` typmod is accepted and
17056 // ignored (the column stores whatever width it's given).
17057 "bit" => {
17058 let varying = matches!(
17059 self.peek(),
17060 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17061 );
17062 if varying {
17063 self.advance();
17064 }
17065 // v7.39 (round 281) — the length used to be parsed and
17066 // dropped, so `bit(3)` accepted a five-bit string.
17067 let n = if matches!(self.peek(), Token::LParen) {
17068 self.parse_paren_size("BIT")?
17069 } else {
17070 0
17071 };
17072 if varying {
17073 ColumnTypeName::BitVarying(n)
17074 } else {
17075 ColumnTypeName::Bit(n)
17076 }
17077 }
17078 "varbit" => {
17079 let n = if matches!(self.peek(), Token::LParen) {
17080 self.parse_paren_size("VARBIT")?
17081 } else {
17082 0
17083 };
17084 ColumnTypeName::BitVarying(n)
17085 }
17086 "xml" => ColumnTypeName::Xml,
17087 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17088 "hstore" => ColumnTypeName::Hstore,
17089 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17090 // `ENUM('a','b','c')`. Storage is TEXT; the value
17091 // list lands on `inline_enum_variants` for the
17092 // engine to validate INSERT cells against. Empty
17093 // value list is a parse error (matches MySQL).
17094 "enum" => {
17095 // Expect the opening `(`.
17096 if !matches!(self.peek(), Token::LParen) {
17097 return Err(self.err(alloc::format!(
17098 "expected '(' after ENUM, got {:?}",
17099 self.peek()
17100 )));
17101 }
17102 self.advance();
17103 let mut variants: Vec<String> = Vec::new();
17104 loop {
17105 match self.advance() {
17106 Token::String(s) => variants.push(s),
17107 other => {
17108 return Err(self.err(alloc::format!(
17109 "ENUM(...) expects string literal variants, got {other:?}"
17110 )));
17111 }
17112 }
17113 match self.peek() {
17114 Token::Comma => {
17115 self.advance();
17116 continue;
17117 }
17118 Token::RParen => {
17119 self.advance();
17120 break;
17121 }
17122 other => {
17123 return Err(self.err(alloc::format!(
17124 "expected ',' or ')' in ENUM(...), got {other:?}"
17125 )));
17126 }
17127 }
17128 }
17129 if variants.is_empty() {
17130 return Err(self.err("ENUM(...) must declare at least one variant".into()));
17131 }
17132 inline_enum_variants = Some(variants);
17133 // Storage is plain TEXT; the variant list lives on
17134 // the ColumnSchema side.
17135 ColumnTypeName::Text
17136 }
17137 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17138 // `SET('a','b','c')`. Same parse shape as ENUM;
17139 // semantics differ (subset rather than pick-one).
17140 "set" => {
17141 if !matches!(self.peek(), Token::LParen) {
17142 return Err(self.err(alloc::format!(
17143 "expected '(' after SET, got {:?}",
17144 self.peek()
17145 )));
17146 }
17147 self.advance();
17148 let mut variants: Vec<String> = Vec::new();
17149 loop {
17150 match self.advance() {
17151 Token::String(s) => variants.push(s),
17152 other => {
17153 return Err(self.err(alloc::format!(
17154 "SET(...) expects string literal variants, got {other:?}"
17155 )));
17156 }
17157 }
17158 match self.peek() {
17159 Token::Comma => {
17160 self.advance();
17161 continue;
17162 }
17163 Token::RParen => {
17164 self.advance();
17165 break;
17166 }
17167 other => {
17168 return Err(self.err(alloc::format!(
17169 "expected ',' or ')' in SET(...), got {other:?}"
17170 )));
17171 }
17172 }
17173 }
17174 if variants.is_empty() {
17175 return Err(self.err("SET(...) must declare at least one variant".into()));
17176 }
17177 inline_set_variants = Some(variants);
17178 ColumnTypeName::Text
17179 }
17180 _other => {
17181 // v7.17.0 Phase 1.4 — unknown ident → defer
17182 // resolution to the engine. Stored as Text in
17183 // ColumnTypeName + the original name carried as
17184 // `user_type_ref` so CREATE TABLE can look up
17185 // user-defined enum / domain types.
17186 user_type_ref = Some(ty_ident.clone());
17187 ColumnTypeName::Text
17188 }
17189 };
17190 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17191 // right after the type keyword. Pre-4.4 SPG consumed +
17192 // discarded the keyword, leaving a customer column
17193 // declared `id INT UNSIGNED NOT NULL` silently accepting
17194 // negative values — a Tier-A correctness drift where
17195 // application invariants (auto-increment-IDs never
17196 // negative) silently broke on cutover. Now: capture as
17197 // a column flag, persist on the schema, enforce at
17198 // INSERT / UPDATE time.
17199 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17200 {
17201 self.advance();
17202 true
17203 } else {
17204 false
17205 };
17206 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17207 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17208 // stores text as UTF-8 always so CHARACTER SET is still a
17209 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17210 // name: it gets classified into a `Collation` variant the
17211 // engine consults at WHERE-eval time. PG `default` /
17212 // `pg_catalog.default` / `C` / `POSIX` collations all
17213 // resolve to `Binary` (the prior behaviour); `_ci` /
17214 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17215 // The schema-qualifier form (`pg_catalog.default`) lexes
17216 // as `Ident '.' Ident` — peek for the `.` and consume both
17217 // halves so it's treated as one collation name. PG's
17218 // `IDENT.IDENT` collation form (which can appear here) is
17219 // resolved by Collation::from_collation_name on the bare
17220 // identifier after the dot.
17221 let mut collation = Collation::Binary;
17222 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17223 // clause was written. The engine needs this to tell an explicit
17224 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17225 // clause at all: both resolve to `Collation::Binary`, but under the
17226 // MySQL dialect the latter takes the folding default collation.
17227 let mut collation_explicit = false;
17228 let mut collation_name: Option<alloc::string::String> = None;
17229 loop {
17230 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17231 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17232 {
17233 self.advance(); // CHARACTER
17234 self.advance(); // SET
17235 if matches!(
17236 self.peek(),
17237 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17238 ) {
17239 self.advance();
17240 }
17241 continue;
17242 }
17243 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17244 self.advance(); // COLLATE
17245 // Accept Ident / QuotedIdent / String AND the
17246 // keyword-tokenised `Default` (PG `pg_catalog.default`
17247 // and bare `DEFAULT` collation names — `default` is a
17248 // reserved word so the lexer hands back Token::Default
17249 // not Token::Ident).
17250 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17251 match this.peek().clone() {
17252 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17253 this.advance();
17254 Some(s)
17255 }
17256 Token::Default => {
17257 this.advance();
17258 Some(alloc::string::String::from("default"))
17259 }
17260 _ => None,
17261 }
17262 };
17263 let raw = if let Some(head) = read_collation_atom(self) {
17264 // Schema-qualified PG form: `pg_catalog.default`.
17265 if matches!(self.peek(), Token::Dot) {
17266 self.advance();
17267 let tail = read_collation_atom(self).unwrap_or_default();
17268 alloc::format!("{head}.{tail}")
17269 } else {
17270 head
17271 }
17272 } else {
17273 alloc::string::String::new()
17274 };
17275 if !raw.is_empty() {
17276 collation_explicit = true;
17277 // v7.39 (round 676) — keep the name too. The enum below
17278 // folds C / POSIX / en_US / default into one value, and
17279 // `pg_attribute.attcollation` has to tell them apart.
17280 // The schema qualifier goes: PG's `pg_catalog.default`
17281 // and a bare `default` name the same collation.
17282 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17283 // encoding suffix. Round 676 used `rsplit('.')` for
17284 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17285 // PG writes `pg_catalog.default` (qualifier) and
17286 // `en_US.utf8` (locale + encoding) with the same
17287 // separator. Only `pg_catalog.` is a qualifier, and it
17288 // is the only one PG's own dumps emit.
17289 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17290 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17291 collation_name = Some(alloc::string::String::from(bare));
17292 let parsed = Collation::from_collation_name(&raw);
17293 // Last COLLATE clause wins, but `Binary` from a
17294 // bare keyword like `default` should not
17295 // silently downgrade a stronger one set earlier
17296 // on the same column. v7.17 only ships one
17297 // non-Binary variant so a simple OR is enough.
17298 if parsed != Collation::Binary {
17299 collation = parsed;
17300 }
17301 }
17302 continue;
17303 }
17304 break;
17305 }
17306 // v7.10.10 — postfix `[]` widens the base type to its array
17307 // type. PG accepts `TYPE[]` after any base type and so does
17308 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17309 // all through; the old "only TEXT[]" note was stale).
17310 if matches!(self.peek(), Token::LBracket) {
17311 self.advance();
17312 if !matches!(self.peek(), Token::RBracket) {
17313 return Err(self.err(alloc::format!(
17314 "TEXT[] takes no dimension; got {:?}",
17315 self.peek()
17316 )));
17317 }
17318 self.advance();
17319 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17320 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17321 // still error here.
17322 ty = match ty {
17323 ColumnTypeName::Text => ColumnTypeName::TextArray,
17324 ColumnTypeName::Int => ColumnTypeName::IntArray,
17325 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17326 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17327 // `[]` grammar. Wire OID 1187.
17328 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17329 // v7.37.5 γ — full PG array-of-scalar family.
17330 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17331 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17332 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17333 // NUMERIC(p, s) loses its precision params at the
17334 // array level (matches PG: `NUMERIC[]` is untyped,
17335 // per-element precision flows through values).
17336 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17337 ColumnTypeName::Date => ColumnTypeName::DateArray,
17338 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17339 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17340 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17341 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17342 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17343 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17344 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17345 // the array level (matches PG semantics where the
17346 // element precision is per-row, not column-wide).
17347 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17348 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17349 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17350 // follow-up.
17351 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17352 other => {
17353 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17354 }
17355 };
17356 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17357 // for INT/TEXT/BIGINT. Anything else is an error.
17358 if matches!(self.peek(), Token::LBracket) {
17359 self.advance();
17360 if !matches!(self.peek(), Token::RBracket) {
17361 return Err(self.err(alloc::format!(
17362 "TYPE[][] second dimension takes no size; got {:?}",
17363 self.peek()
17364 )));
17365 }
17366 self.advance();
17367 ty = match ty {
17368 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17369 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17370 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17371 // v7.39 (read01 round 75) — bool[][].
17372 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17373 other => {
17374 return Err(self.err(alloc::format!(
17375 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17376 TEXT[][] only; got {other:?}"
17377 )));
17378 }
17379 };
17380 }
17381 }
17382 Ok((
17383 ty,
17384 implied_auto_increment,
17385 implied_not_null,
17386 user_type_ref,
17387 collation,
17388 collation_explicit,
17389 collation_name,
17390 is_unsigned,
17391 inline_enum_variants,
17392 inline_set_variants,
17393 mysql_int_width,
17394 mysql_fsp,
17395 mysql_declared_timestamp,
17396 mysql_float_md,
17397 ))
17398 }
17399
17400 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17401 // v7.20 — PG reserves the table-constraint keywords, so a
17402 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17403 // malformed constraint clause (e.g. `UNIQUE a` missing its
17404 // parens), not a column named "unique". Since v7.17's
17405 // unknown-type leniency (`user_type_ref`) such a clause
17406 // would otherwise parse as a column with a user-defined
17407 // type — silently accepting invalid DDL. Quoted
17408 // identifiers ("unique" / `unique`) remain valid names.
17409 if let Token::Ident(s) = self.peek()
17410 && [
17411 "unique",
17412 "primary",
17413 "foreign",
17414 "constraint",
17415 "check",
17416 "references",
17417 "exclude",
17418 ]
17419 .iter()
17420 .any(|kw| s.eq_ignore_ascii_case(kw))
17421 {
17422 return Err(self.err(alloc::format!(
17423 "unexpected reserved keyword '{s}' at start of column definition \
17424 (malformed table constraint?)"
17425 )));
17426 }
17427 let name_tok = self.pos;
17428 let name = self.expect_ident_like()?;
17429 // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17430 // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17431 // information_schema, and in SHOW CREATE (measured). SPG folded
17432 // an unquoted name, so a table restored from a dump reported
17433 // names the application had never written.
17434 //
17435 // The written form comes back from the source span, which only
17436 // the MySQL dialect keeps. The span runs to the START of the
17437 // next token, so a comment or unusual spacing between them
17438 // arrives with it — hence the check that what came back is the
17439 // same identifier. It is not decoration: without it,
17440 // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17441 // `MyCol /* c */`.
17442 let name = self
17443 .source_span(name_tok, name_tok)
17444 .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17445 .filter(|raw| raw.eq_ignore_ascii_case(&name))
17446 .map_or(name, alloc::string::String::from);
17447 let (
17448 ty,
17449 implied_auto_increment,
17450 implied_not_null,
17451 user_type_ref,
17452 collation,
17453 collation_explicit,
17454 collation_name,
17455 is_unsigned,
17456 inline_enum_variants,
17457 inline_set_variants,
17458 mysql_int_width,
17459 mysql_fsp,
17460 mysql_declared_timestamp,
17461 mysql_float_md,
17462 ) = self.parse_type_with_implied_flags()?;
17463 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17464 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17465 // each at most once.
17466 let mut default: Option<Expr> = None;
17467 let mut nullable = !implied_not_null;
17468 let mut nullability_seen = implied_not_null;
17469 let mut auto_increment = implied_auto_increment;
17470 let mut is_primary_key = false;
17471 let mut is_unique = false;
17472 let mut unique_nulls_not_distinct = false;
17473 let mut constraint_deferrable = false;
17474 let mut constraint_initially_deferred = false;
17475 let mut check: Option<Expr> = None;
17476 let mut on_update_runtime: Option<Expr> = None;
17477 let mut generated_stored_expr: Option<Box<Expr>> = None;
17478 let mut identity_always = false;
17479 loop {
17480 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17481 // not-null constraints by name and pg_dump emits them
17482 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17483 // NOT NULL`. Accept and discard the name; whatever
17484 // constraint follows is parsed by the arms below.
17485 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17486 // v7.39 (round 308, V29) — a name on an inline
17487 // REFERENCES belongs to the FOREIGN KEY, and the caller
17488 // (`parse_column_def_with_fk`) is what builds it, so
17489 // leave the whole clause for it. Dropping the name here
17490 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17491 // as the synthesised `c_pid_fkey` — which then could
17492 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17493 // `advance()` takes tokens by `mem::replace`, so there
17494 // is no rewinding once consumed.
17495 if matches!(
17496 self.tokens.get(self.pos + 2),
17497 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17498 ) {
17499 break;
17500 }
17501 self.advance();
17502 let _name = self.expect_ident_like()?;
17503 continue;
17504 }
17505 // v7.39 (round 379) — MySQL's SHORT generated-column form
17506 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17507 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17508 // below), but hand-written schemas and app migrations use this.
17509 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17510 // SPG computes-and-stores either way, like the long form.
17511 if matches!(self.peek(), Token::As) {
17512 self.advance();
17513 if !matches!(self.peek(), Token::LParen) {
17514 return Err(self.err(alloc::format!(
17515 "expected '(' after AS in a generated column, got {:?}",
17516 self.peek()
17517 )));
17518 }
17519 self.advance();
17520 let expr = self.parse_expr(0)?;
17521 if !matches!(self.peek(), Token::RParen) {
17522 return Err(self.err(alloc::format!(
17523 "expected ')' after AS (<expr>), got {:?}",
17524 self.peek()
17525 )));
17526 }
17527 self.advance();
17528 if matches!(self.peek(), Token::Ident(s)
17529 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17530 {
17531 self.advance();
17532 }
17533 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17534 continue;
17535 }
17536 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17537 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17538 // the modern replacement for SERIAL in hand-written
17539 // schemas). Both flavours map onto the auto-increment
17540 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17541 // ALWAYS's reject-explicit-values nuance is documented
17542 // leniency. Generated EXPRESSION columns
17543 // (`AS (expr) STORED`) are not supported: error loudly
17544 // instead of silently storing NULLs.
17545 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17546 self.advance();
17547 let mut saw_generated_always = false;
17548 match self.peek().clone() {
17549 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17550 self.advance();
17551 saw_generated_always = true;
17552 }
17553 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17554 self.advance();
17555 if !matches!(self.peek(), Token::Default) {
17556 return Err(self.err(alloc::format!(
17557 "expected DEFAULT after GENERATED BY, got {:?}",
17558 self.peek()
17559 )));
17560 }
17561 self.advance();
17562 }
17563 other => {
17564 return Err(self.err(alloc::format!(
17565 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17566 )));
17567 }
17568 }
17569 if !matches!(self.peek(), Token::As) {
17570 return Err(self.err(alloc::format!(
17571 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17572 self.peek()
17573 )));
17574 }
17575 self.advance();
17576 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17577 // ( <expr> ) STORED` stored computed-column. The
17578 // expression is captured for the engine to recompute
17579 // on every INSERT / UPDATE. v7.37.7 accepts the
17580 // STORED keyword only; PG also has VIRTUAL, which
17581 // v7.37.7 carves out (sentori only uses STORED).
17582 if matches!(self.peek(), Token::LParen) {
17583 self.advance();
17584 let expr = self.parse_expr(0)?;
17585 if !matches!(self.peek(), Token::RParen) {
17586 return Err(self.err(alloc::format!(
17587 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17588 self.peek()
17589 )));
17590 }
17591 self.advance();
17592 let stored = match self.peek() {
17593 Token::Ident(s) | Token::QuotedIdent(s)
17594 if s.eq_ignore_ascii_case("stored") =>
17595 {
17596 self.advance();
17597 true
17598 }
17599 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17600 // generated columns. SPG computes them on write and
17601 // persists like STORED; the two are observably
17602 // identical for query results (the value, recompute
17603 // on base-column change, and NOT NULL enforcement all
17604 // match), so a PG 18 schema/dump using VIRTUAL loads
17605 // and behaves correctly. The compute-on-read storage
17606 // saving is an invisible internal difference.
17607 Token::Ident(s) | Token::QuotedIdent(s)
17608 if s.eq_ignore_ascii_case("virtual") =>
17609 {
17610 self.advance();
17611 false
17612 }
17613 other => {
17614 return Err(self.err(alloc::format!(
17615 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17616 got {other:?}"
17617 )));
17618 }
17619 };
17620 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17621 generated_stored_expr = Some(Box::new(expr));
17622 continue;
17623 }
17624 self.expect_keyword_ident("identity")?;
17625 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17626 // consume the balanced parens and discard (SPG's
17627 // auto-increment is max+1-scan based).
17628 if matches!(self.peek(), Token::LParen) {
17629 let mut depth = 0usize;
17630 loop {
17631 match self.advance() {
17632 Token::LParen => depth += 1,
17633 Token::RParen => {
17634 depth -= 1;
17635 if depth == 0 {
17636 break;
17637 }
17638 }
17639 Token::Eof => {
17640 return Err(self.err(
17641 "unterminated sequence-options parens after IDENTITY".into(),
17642 ));
17643 }
17644 _ => {}
17645 }
17646 }
17647 }
17648 auto_increment = true;
17649 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17650 // can reject explicit non-DEFAULT INSERT values (unless
17651 // OVERRIDING SYSTEM VALUE) the way PG does.
17652 identity_always = saw_generated_always;
17653 // PG identity columns are implicitly NOT NULL.
17654 nullable = false;
17655 continue;
17656 }
17657 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17658 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17659 // is accepted today. The "ON" token is an Ident
17660 // (not reserved) — peek before consuming.
17661 if matches!(self.peek(), Token::On)
17662 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17663 {
17664 self.advance(); // ON
17665 self.advance(); // update
17666 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17667 let next = self.peek().clone();
17668 match next {
17669 Token::Ident(s) | Token::QuotedIdent(s)
17670 if s.eq_ignore_ascii_case("current_timestamp") =>
17671 {
17672 self.advance();
17673 // Optional `(N)` precision.
17674 if matches!(self.peek(), Token::LParen) {
17675 self.advance();
17676 if !matches!(self.peek(), Token::Integer(_)) {
17677 return Err(self.err(alloc::format!(
17678 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17679 self.peek()
17680 )));
17681 }
17682 self.advance();
17683 if !matches!(self.peek(), Token::RParen) {
17684 return Err(self.err(alloc::format!(
17685 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17686 self.peek()
17687 )));
17688 }
17689 self.advance();
17690 }
17691 on_update_runtime = Some(Expr::FunctionCall {
17692 name: "now".into(),
17693 args: Vec::new(),
17694 });
17695 continue;
17696 }
17697 other => {
17698 return Err(self.err(alloc::format!(
17699 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17700 )));
17701 }
17702 }
17703 }
17704 if matches!(self.peek(), Token::Default) {
17705 if default.is_some() {
17706 return Err(self.err("DEFAULT specified twice".into()));
17707 }
17708 self.advance();
17709 default = Some(self.parse_expr(0)?);
17710 continue;
17711 }
17712 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17713 // token with NOT NULL and sits EARLIER in the loop than the
17714 // deferrability arm, so without the lookahead it was reported as
17715 // "NOT NULL specified twice" (or "expected NULL after NOT").
17716 if matches!(self.peek(), Token::Not)
17717 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17718 {
17719 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17720 self.consume_optional_deferrable_clauses()?;
17721 continue;
17722 }
17723 if matches!(self.peek(), Token::Not) {
17724 if nullability_seen {
17725 return Err(self.err("NOT NULL specified twice".into()));
17726 }
17727 self.advance();
17728 if !matches!(self.peek(), Token::Null) {
17729 return Err(self.err(format!(
17730 "expected NULL after NOT in column def, got {:?}",
17731 self.peek()
17732 )));
17733 }
17734 self.advance();
17735 nullable = false;
17736 nullability_seen = true;
17737 continue;
17738 }
17739 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17740 // "this column is nullable" marker (the default in
17741 // standard SQL anyway). mysqldump emits it routinely
17742 // (`col TYPE NULL DEFAULT NULL` for nullable
17743 // timestamps etc). Accept + no-op.
17744 if matches!(self.peek(), Token::Null) {
17745 if nullability_seen && !nullable {
17746 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17747 // sentence, PG18-measured (the table name is the
17748 // caller's; the column half is exact).
17749 return Err(self.err(alloc::format!(
17750 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17751 )));
17752 }
17753 self.advance();
17754 nullable = true;
17755 nullability_seen = true;
17756 continue;
17757 }
17758 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17759 // arrives as a bare Ident. Match either, case-insensitive.
17760 if let Token::Ident(s) = self.peek()
17761 && (s.eq_ignore_ascii_case("auto_increment")
17762 || s.eq_ignore_ascii_case("autoincrement"))
17763 {
17764 if auto_increment {
17765 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17766 }
17767 self.advance();
17768 auto_increment = true;
17769 continue;
17770 }
17771 // v7.9.13 — inline `PRIMARY KEY` column constraint
17772 // (mailrs F1). Implies `NOT NULL`. The engine creates
17773 // a BTree index for the PK column at CREATE TABLE time
17774 // so FK parent-side index lookups resolve.
17775 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17776 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17777 // spelling was a parse error, so a pg_dump carrying one stopped
17778 // mid-restore. The clauses are consumed by the same helper the FK
17779 // path has used since round 288 and recorded nowhere: SPG enforces
17780 // the constraint IMMEDIATELY either way, which fails earlier than
17781 // PG inside a transaction that violates-then-repairs — a refusal,
17782 // not a wrong answer. True deferral is the open remainder of F08.
17783 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17784 || (matches!(self.peek(), Token::Not)
17785 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17786 {
17787 // v7.39 (round 711) — CARRIED now (the storing half of
17788 // F08); round 621 only consumed.
17789 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17790 constraint_deferrable |= d;
17791 constraint_initially_deferred |= idef;
17792 continue;
17793 }
17794 if let Token::Ident(s) = self.peek()
17795 && s.eq_ignore_ascii_case("primary")
17796 {
17797 if is_primary_key {
17798 return Err(self.err("PRIMARY KEY specified twice".into()));
17799 }
17800 // Peek-ahead for the required `KEY` token.
17801 let next = self.tokens.get(self.pos + 1);
17802 let next_is_key = matches!(
17803 next,
17804 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17805 );
17806 if !next_is_key {
17807 return Err(self.err(format!(
17808 "expected KEY after PRIMARY in column def, got {:?}",
17809 next
17810 )));
17811 }
17812 self.advance(); // PRIMARY
17813 self.advance(); // KEY
17814 is_primary_key = true;
17815 if nullability_seen && nullable {
17816 return Err(self.err(
17817 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17818 ));
17819 }
17820 nullable = false;
17821 nullability_seen = true;
17822 continue;
17823 }
17824 // v7.13.0 — inline `UNIQUE` column constraint
17825 // (mailrs round-5 G2). Fold into a single-column
17826 // table-level UNIQUE at CREATE TABLE post-process time.
17827 if let Token::Ident(s) = self.peek()
17828 && s.eq_ignore_ascii_case("unique")
17829 {
17830 if is_unique {
17831 return Err(self.err("UNIQUE specified twice".into()));
17832 }
17833 self.advance();
17834 is_unique = true;
17835 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17836 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17837 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17838 let n1 = self.tokens.get(self.pos + 1);
17839 let n2 = self.tokens.get(self.pos + 2);
17840 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17841 self.advance(); // NULLS
17842 self.advance(); // NOT
17843 self.advance(); // DISTINCT
17844 unique_nulls_not_distinct = true;
17845 } else if matches!(n1, Some(Token::Distinct)) {
17846 self.advance(); // NULLS
17847 self.advance(); // DISTINCT
17848 }
17849 }
17850 continue;
17851 }
17852 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17853 // (mailrs round-5 G3). PG semantics: column-level
17854 // CHECK is equivalent to a table-level CHECK. Multiple
17855 // inline CHECKs on the same column AND together.
17856 if let Token::Ident(s) = self.peek()
17857 && s.eq_ignore_ascii_case("check")
17858 {
17859 self.advance();
17860 if !matches!(self.peek(), Token::LParen) {
17861 return Err(self.err(alloc::format!(
17862 "expected '(' after CHECK in column def, got {:?}",
17863 self.peek()
17864 )));
17865 }
17866 self.advance();
17867 let pred = self.parse_expr(0)?;
17868 if !matches!(self.peek(), Token::RParen) {
17869 return Err(self.err(alloc::format!(
17870 "expected ')' to close CHECK predicate, got {:?}",
17871 self.peek()
17872 )));
17873 }
17874 self.advance();
17875 check = Some(match check.take() {
17876 Some(prev) => Expr::Binary {
17877 op: BinOp::And,
17878 lhs: Box::new(prev),
17879 rhs: Box::new(pred),
17880 },
17881 None => pred,
17882 });
17883 continue;
17884 }
17885 break;
17886 }
17887 Ok(ColumnDef {
17888 name,
17889 ty,
17890 nullable,
17891 default,
17892 auto_increment,
17893 is_primary_key,
17894 is_unique,
17895 unique_nulls_not_distinct,
17896 constraint_deferrable,
17897 constraint_initially_deferred,
17898 check,
17899 user_type_ref,
17900 on_update_runtime,
17901 collation,
17902 collation_explicit,
17903 collation_name,
17904 is_unsigned,
17905 inline_enum_variants,
17906 inline_set_variants,
17907 generated_stored_expr,
17908 identity_always,
17909 mysql_int_width,
17910 mysql_fsp,
17911 mysql_declared_timestamp,
17912 mysql_float_md,
17913 })
17914 }
17915
17916 /// `NUMERIC` may appear without parameters, with one (precision
17917 /// only, scale=0), or with both. Returns `(precision, scale)` with
17918 /// 0 = unspecified for the bare form.
17919 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17920 if !matches!(self.peek(), Token::LParen) {
17921 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17922 // we surface it as precision=0 to mean "unconstrained" so
17923 // the engine doesn't need a separate variant.
17924 return Ok((0, 0));
17925 }
17926 self.advance();
17927 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17928 // it words the out-of-range case with the value it saw. SPG
17929 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17930 // accepts failed to parse at all; values wider than i128 are
17931 // carried by the arbitrary-precision form.
17932 let precision = match self.advance() {
17933 Token::Integer(n) if (1..=1000).contains(&n) => {
17934 u16::try_from(n).expect("range-checked")
17935 }
17936 Token::Integer(n) => {
17937 return Err(ParseError {
17938 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17939 token_pos: self.consumed_pos(),
17940 });
17941 }
17942 other => {
17943 return Err(ParseError {
17944 message: format!(
17945 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17946 ),
17947 token_pos: self.consumed_pos(),
17948 });
17949 }
17950 };
17951 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17952 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17953 // then overflows). A negative scale rounds to tens / hundreds / …
17954 let scale = if matches!(self.peek(), Token::Comma) {
17955 self.advance();
17956 let neg = if matches!(self.peek(), Token::Minus) {
17957 self.advance();
17958 true
17959 } else {
17960 false
17961 };
17962 match self.advance() {
17963 Token::Integer(n) => {
17964 let signed = if neg { -n } else { n };
17965 if !(-1000..=1000).contains(&signed) {
17966 return Err(ParseError {
17967 message: format!(
17968 "NUMERIC scale {signed} must be between -1000 and 1000"
17969 ),
17970 token_pos: self.consumed_pos(),
17971 });
17972 }
17973 i16::try_from(signed).expect("range-checked")
17974 }
17975 other => {
17976 return Err(ParseError {
17977 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17978 token_pos: self.consumed_pos(),
17979 });
17980 }
17981 }
17982 } else {
17983 0
17984 };
17985 if !matches!(self.peek(), Token::RParen) {
17986 return Err(self.err(format!(
17987 "expected ')' to close NUMERIC params, got {:?}",
17988 self.peek()
17989 )));
17990 }
17991 self.advance();
17992 Ok((precision, scale))
17993 }
17994
17995 /// Parse `(N)` where `N` is a positive integer literal — used by the
17996 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17997 /// for the error message.
17998 /// v6.0.1: parse the optional `USING <encoding>` clause that
17999 /// follows `VECTOR(N)` in a column definition. Missing clause
18000 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18001 /// ident → `ParseError` listing the encodings recognised today.
18002 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18003 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18004 return Ok(VecEncoding::F32);
18005 }
18006 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18007 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18008 // consume the token when the very next token is a known
18009 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18010 // `USING` for the caller — it's the rewrite-expression form.
18011 let n1 = self.tokens.get(self.pos + 1);
18012 let next_is_encoding = matches!(
18013 n1,
18014 Some(Token::Ident(s))
18015 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18016 );
18017 if !next_is_encoding {
18018 return Ok(VecEncoding::F32);
18019 }
18020 self.advance();
18021 let enc_ident = match self.advance() {
18022 Token::Ident(s) => s,
18023 other => {
18024 return Err(self.err(format!(
18025 "expected vector encoding after USING, got {other:?}"
18026 )));
18027 }
18028 };
18029 match enc_ident.to_ascii_lowercase().as_str() {
18030 "sq8" => Ok(VecEncoding::Sq8),
18031 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18032 // binary16 per-element storage.
18033 "half" => Ok(VecEncoding::F16),
18034 other => Err(self.err(format!(
18035 "unknown vector encoding {other:?}; supported: SQ8, HALF"
18036 ))),
18037 }
18038 }
18039
18040 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18041 /// without consuming it. Returns `Some(N)` when the next
18042 /// tokens are `( <int> )`; None otherwise. Used by the
18043 /// TINYINT classifier to decide whether to map to Bool or
18044 /// SmallInt.
18045 fn peek_optional_paren_size_value(&self) -> Option<i64> {
18046 if !matches!(self.peek(), Token::LParen) {
18047 return None;
18048 }
18049 let next = self.tokens.get(self.pos + 1)?;
18050 let n = match next {
18051 Token::Integer(n) => *n,
18052 _ => return None,
18053 };
18054 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18055 return None;
18056 }
18057 Some(n)
18058 }
18059
18060 /// v7.14.0 — consume an optional MySQL display-width
18061 /// parenthesised number after an integer type, returning
18062 /// nothing. `TINYINT(1)` etc.
18063 /// v7.39 (round 360) — does the parenthesised group ahead contain a
18064 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18065 fn peek_paren_has_comma(&self) -> bool {
18066 let mut i = self.pos + 1;
18067 let mut depth = 1usize;
18068 while depth > 0 {
18069 match self.tokens.get(i) {
18070 Some(Token::LParen) => depth += 1,
18071 Some(Token::RParen) => depth -= 1,
18072 Some(Token::Comma) if depth == 1 => return true,
18073 None | Some(Token::Eof) => return false,
18074 _ => {}
18075 }
18076 i += 1;
18077 }
18078 false
18079 }
18080
18081 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18082 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18083 /// fractional-seconds precision that drives write truncation and render
18084 /// padding, where `consume_optional_paren_size` throws it away.
18085 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18086 fn take_optional_paren_size(&mut self) -> Option<u8> {
18087 let Some(Token::Integer(n)) = self
18088 .tokens
18089 .get(self.pos + 1)
18090 .filter(|_| matches!(self.peek(), Token::LParen))
18091 .cloned()
18092 else {
18093 self.consume_optional_paren_size();
18094 return None;
18095 };
18096 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18097 self.consume_optional_paren_size();
18098 return None;
18099 }
18100 self.consume_optional_paren_size();
18101 u8::try_from(n).ok()
18102 }
18103
18104 fn consume_optional_paren_size(&mut self) {
18105 if !matches!(self.peek(), Token::LParen) {
18106 return;
18107 }
18108 self.advance();
18109 // Skip until matching RParen (allow nested or any tokens).
18110 let mut depth = 1usize;
18111 while depth > 0 {
18112 match self.peek() {
18113 Token::LParen => depth += 1,
18114 Token::RParen => depth -= 1,
18115 Token::Eof => return,
18116 _ => {}
18117 }
18118 self.advance();
18119 }
18120 }
18121
18122 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18123 if !matches!(self.peek(), Token::LParen) {
18124 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18125 }
18126 self.advance();
18127 let n = match self.advance() {
18128 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18129 message: format!("{label} size too large: {n}"),
18130 token_pos: self.consumed_pos(),
18131 })?,
18132 other => {
18133 return Err(ParseError {
18134 message: format!("expected positive integer {label} size, got {other:?}"),
18135 token_pos: self.consumed_pos(),
18136 });
18137 }
18138 };
18139 if !matches!(self.peek(), Token::RParen) {
18140 return Err(self.err(format!(
18141 "expected ')' after {label} size, got {:?}",
18142 self.peek()
18143 )));
18144 }
18145 self.advance();
18146 Ok(n)
18147 }
18148
18149 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18150 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18151 /// key, like MySQL) whose action skips conflicting rows.
18152 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18153 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18154 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18155 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18156 /// common bulk-upsert spellings —
18157 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18158 /// REPLACE INTO t SELECT …
18159 /// — were a parse error / a duplicate-key failure respectively.
18160 ///
18161 /// Precedence: an explicitly written clause beats a statement-level flag.
18162 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18163 /// implicit `REPLACE` and `IGNORE` lowerings.
18164 fn parse_insert_conflict_clause(
18165 &mut self,
18166 replace: bool,
18167 ignore: bool,
18168 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18169 if let Some(c) = self.parse_optional_on_duplicate_key()? {
18170 return Ok(Some(c));
18171 }
18172 if let Some(c) = self.parse_optional_on_conflict()? {
18173 return Ok(Some(c));
18174 }
18175 if replace {
18176 // REPLACE INTO = delete-then-insert, which PG spells as
18177 // `ON CONFLICT DO UPDATE SET` over every column; the engine
18178 // reads an empty assignment list as "take the incoming row".
18179 return Ok(Some(crate::ast::OnConflictClause {
18180 target_columns: Vec::new(),
18181 index_where: None,
18182 constraint_name: None,
18183 mysql_lowered: true,
18184 action: crate::ast::OnConflictAction::Update {
18185 assignments: Vec::new(),
18186 where_: None,
18187 },
18188 }));
18189 }
18190 if ignore {
18191 return Ok(Some(Self::insert_ignore_clause()));
18192 }
18193 Ok(None)
18194 }
18195
18196 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18197 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18198 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18199 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18200 fn parse_optional_on_duplicate_key(
18201 &mut self,
18202 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18203 if !(matches!(self.peek(), Token::On)
18204 && matches!(self.tokens.get(self.pos + 1),
18205 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18206 {
18207 return Ok(None);
18208 }
18209 self.advance(); // ON
18210 self.advance(); // DUPLICATE
18211 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18212 return Err(self.err(format!(
18213 "expected KEY after ON DUPLICATE, got {:?}",
18214 self.peek()
18215 )));
18216 }
18217 self.advance();
18218 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18219 return Err(self.err(format!(
18220 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18221 self.peek()
18222 )));
18223 }
18224 self.advance();
18225 let mut assignments: Vec<(String, Expr)> = Vec::new();
18226 loop {
18227 let col = self.expect_ident_like()?;
18228 if !matches!(self.peek(), Token::Eq) {
18229 return Err(self.err(format!(
18230 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18231 self.peek()
18232 )));
18233 }
18234 self.advance();
18235 let mut expr = self.parse_expr(0)?;
18236 Self::rewrite_mysql_values_refs(&mut expr);
18237 assignments.push((col, expr));
18238 if matches!(self.peek(), Token::Comma) {
18239 self.advance();
18240 continue;
18241 }
18242 break;
18243 }
18244 Ok(Some(crate::ast::OnConflictClause {
18245 target_columns: Vec::new(),
18246 index_where: None,
18247 constraint_name: None,
18248 mysql_lowered: true,
18249 action: crate::ast::OnConflictAction::Update {
18250 assignments,
18251 where_: None,
18252 },
18253 }))
18254 }
18255
18256 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18257 crate::ast::OnConflictClause {
18258 target_columns: Vec::new(),
18259 index_where: None,
18260 constraint_name: None,
18261 mysql_lowered: true,
18262 action: crate::ast::OnConflictAction::Nothing,
18263 }
18264 }
18265
18266 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18267 debug_assert!(
18268 matches!(self.peek(), Token::Insert)
18269 || (replace
18270 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18271 );
18272 self.advance();
18273 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18274 // would raise a duplicate-key error instead of failing the statement,
18275 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18276 // plain ident to the lexer; only the MySQL dialect accepts it here.
18277 let ignore = self.mysql_dialect
18278 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18279 if ignore {
18280 self.advance();
18281 }
18282 if !matches!(self.peek(), Token::Into) {
18283 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18284 }
18285 self.advance();
18286 let table = self.expect_ident_like()?;
18287 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18288 // grammar requires the AS keyword here (a bare identifier would be
18289 // ambiguous with a column list). The alias is what the ON CONFLICT
18290 // DO UPDATE expressions refer to the target row by.
18291 let alias = if matches!(self.peek(), Token::As) {
18292 self.advance();
18293 Some(self.expect_ident_like()?)
18294 } else {
18295 None
18296 };
18297 // v7.39 (round 428) — MySQL's SET-form INSERT:
18298 // INSERT INTO t SET a = 1, b = 'x'
18299 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18300 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18301 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18302 // measured). So it lowers to the column list + one VALUES row and
18303 // rejoins the ordinary path, which already handles every one of
18304 // those. PG has no such spelling, hence the dialect gate.
18305 if self.mysql_dialect
18306 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18307 {
18308 self.advance(); // SET
18309 let mut names = Vec::new();
18310 let mut values = Vec::new();
18311 loop {
18312 names.push(self.expect_ident_like()?);
18313 if !matches!(self.peek(), Token::Eq) {
18314 return Err(self.err(alloc::format!(
18315 "expected '=' in INSERT … SET, got {:?}",
18316 self.peek()
18317 )));
18318 }
18319 self.advance();
18320 // `SET a = DEFAULT` rides the same `__column_default` marker
18321 // the VALUES-row and UPDATE-SET paths use; the INSERT
18322 // executor resolves it against the target column.
18323 if matches!(self.peek(), Token::Default) {
18324 self.advance();
18325 values.push(Expr::FunctionCall {
18326 name: "__column_default".to_string(),
18327 args: Vec::new(),
18328 });
18329 } else {
18330 values.push(self.parse_expr(0)?);
18331 }
18332 if matches!(self.peek(), Token::Comma) {
18333 self.advance();
18334 continue;
18335 }
18336 break;
18337 }
18338 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18339 let returning = self.parse_optional_returning()?;
18340 return Ok(Statement::Insert(InsertStatement {
18341 ctes: Vec::new(),
18342 table,
18343 alias,
18344 columns: Some(names),
18345 rows: alloc::vec![values],
18346 select_source: None,
18347 // MySQL's SET form has no `OVERRIDING …` clause (that is
18348 // PG's identity-column spelling).
18349 overriding: Overriding::None,
18350 mysql_ignore: ignore,
18351 on_conflict,
18352 returning,
18353 }));
18354 }
18355 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18356 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18357 // a parenthesized query source instead (PG select_with_parens:
18358 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18359 // both keywords are reserved in PG, so no column list can start
18360 // with them.
18361 let columns = if matches!(self.peek(), Token::LParen) {
18362 self.advance();
18363 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18364 let select_stmt = if self.peek_is_with_kw() {
18365 self.advance();
18366 self.parse_nested_with_select()?
18367 } else {
18368 match self.parse_select_stmt()? {
18369 Statement::Select(s) => s,
18370 other => {
18371 return Err(self.err(alloc::format!(
18372 "expected SELECT in parenthesized INSERT source, got {other:?}"
18373 )));
18374 }
18375 }
18376 };
18377 if !matches!(self.peek(), Token::RParen) {
18378 return Err(self.err(format!(
18379 "expected ')' after parenthesized INSERT source, got {:?}",
18380 self.peek()
18381 )));
18382 }
18383 self.advance();
18384 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18385 let returning = self.parse_optional_returning()?;
18386 return Ok(Statement::Insert(InsertStatement {
18387 ctes: Vec::new(),
18388 table,
18389 alias: alias.clone(),
18390 columns: None,
18391 rows: Vec::new(),
18392 select_source: Some(Box::new(select_stmt)),
18393 on_conflict,
18394 returning,
18395 overriding: Overriding::None,
18396 mysql_ignore: ignore,
18397 }));
18398 }
18399 let mut names = Vec::new();
18400 loop {
18401 names.push(self.expect_ident_like()?);
18402 match self.peek() {
18403 Token::Comma => {
18404 self.advance();
18405 }
18406 Token::RParen => {
18407 self.advance();
18408 break;
18409 }
18410 other => {
18411 return Err(self.err(format!(
18412 "expected ',' or ')' in INSERT column list, got {other:?}"
18413 )));
18414 }
18415 }
18416 }
18417 Some(names)
18418 } else {
18419 None
18420 };
18421 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18422 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18423 // is captured on the statement so the engine can apply PG's
18424 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18425 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18426 {
18427 self.advance();
18428 let which = self.expect_ident_like()?;
18429 let ov = if which.eq_ignore_ascii_case("system") {
18430 Overriding::System
18431 } else if which.eq_ignore_ascii_case("user") {
18432 Overriding::User
18433 } else {
18434 return Err(self.err(format!(
18435 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18436 )));
18437 };
18438 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18439 return Err(self.err(format!(
18440 "expected VALUE after OVERRIDING {}, got {:?}",
18441 which.to_ascii_uppercase(),
18442 self.peek()
18443 )));
18444 }
18445 self.advance();
18446 ov
18447 } else {
18448 Overriding::None
18449 };
18450 // `INSERT INTO t DEFAULT VALUES` — a single row made
18451 // entirely of column defaults. Lower to the permuted
18452 // column-list path with an empty list: every schema column
18453 // is unmapped, so the engine fills each from its default
18454 // (serials advance, plain defaults evaluate, the rest NULL).
18455 if matches!(self.peek(), Token::Default) {
18456 self.advance();
18457 if !matches!(self.peek(), Token::Values) {
18458 return Err(self.err(format!(
18459 "expected VALUES after DEFAULT in INSERT, got {:?}",
18460 self.peek()
18461 )));
18462 }
18463 self.advance();
18464 if columns.is_some() {
18465 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18466 }
18467 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18468 let returning = self.parse_optional_returning()?;
18469 return Ok(Statement::Insert(InsertStatement {
18470 ctes: Vec::new(),
18471 table,
18472 alias: alias.clone(),
18473 columns: Some(Vec::new()),
18474 rows: alloc::vec![Vec::new()],
18475 select_source: None,
18476 on_conflict,
18477 returning,
18478 overriding,
18479 mysql_ignore: ignore,
18480 }));
18481 }
18482 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18483 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18484 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18485 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18486 // own WITH comes before INSERT).
18487 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18488 let select_stmt = if self.peek_is_with_kw() {
18489 self.advance();
18490 self.parse_nested_with_select()?
18491 } else {
18492 match self.parse_select_stmt()? {
18493 Statement::Select(s) => s,
18494 other => {
18495 return Err(self.err(alloc::format!(
18496 "expected SELECT after INSERT INTO ... target, got {other:?}"
18497 )));
18498 }
18499 }
18500 };
18501 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18502 let returning = self.parse_optional_returning()?;
18503 return Ok(Statement::Insert(InsertStatement {
18504 ctes: Vec::new(),
18505 table,
18506 alias: alias.clone(),
18507 columns,
18508 rows: Vec::new(),
18509 select_source: Some(Box::new(select_stmt)),
18510 on_conflict,
18511 returning,
18512 overriding,
18513 mysql_ignore: ignore,
18514 }));
18515 }
18516 if !matches!(self.peek(), Token::Values) {
18517 return Err(self.err(format!(
18518 "expected VALUES or SELECT after table name, got {:?}",
18519 self.peek()
18520 )));
18521 }
18522 self.advance();
18523 if !matches!(self.peek(), Token::LParen) {
18524 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18525 }
18526 let mut rows = Vec::new();
18527 loop {
18528 // Each iteration consumes one `(expr, expr, …)` tuple.
18529 if !matches!(self.peek(), Token::LParen) {
18530 return Err(self.err(format!(
18531 "expected '(' for next VALUES tuple, got {:?}",
18532 self.peek()
18533 )));
18534 }
18535 self.advance();
18536 let mut tuple = Vec::new();
18537 loop {
18538 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18539 // the column's declared default for that slot. Rides out as the
18540 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18541 // path uses; the INSERT executor resolves it per target column.
18542 if matches!(self.peek(), Token::Default) {
18543 self.advance();
18544 tuple.push(Expr::FunctionCall {
18545 name: "__column_default".to_string(),
18546 args: Vec::new(),
18547 });
18548 } else {
18549 tuple.push(self.parse_expr(0)?);
18550 }
18551 match self.peek() {
18552 Token::Comma => {
18553 self.advance();
18554 }
18555 Token::RParen => {
18556 self.advance();
18557 break;
18558 }
18559 other => {
18560 return Err(self.err(format!(
18561 "expected ',' or ')' in VALUES tuple, got {other:?}"
18562 )));
18563 }
18564 }
18565 }
18566 if tuple.is_empty() {
18567 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18568 }
18569 rows.push(tuple);
18570 // Continue with comma-separated tuples.
18571 if matches!(self.peek(), Token::Comma) {
18572 self.advance();
18573 } else {
18574 break;
18575 }
18576 }
18577 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18578 // to ON CONFLICT DO UPDATE with an empty conflict target
18579 // (the engine picks the table's first unique index, which
18580 // matches MySQL's any-unique-key behaviour for the common
18581 // single-key case). `VALUES(col)` in the assignments is
18582 // MySQL's spelling of EXCLUDED.col.
18583 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18584 let returning = self.parse_optional_returning()?;
18585 Ok(Statement::Insert(InsertStatement {
18586 ctes: Vec::new(),
18587 table,
18588 alias,
18589 columns,
18590 rows,
18591 select_source: None,
18592 on_conflict,
18593 returning,
18594 overriding,
18595 mysql_ignore: ignore,
18596 }))
18597 }
18598
18599 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18600 /// the incoming row's value — exactly PG's EXCLUDED.col.
18601 fn rewrite_mysql_values_refs(e: &mut Expr) {
18602 match e {
18603 Expr::FunctionCall { name, args }
18604 if name.eq_ignore_ascii_case("values")
18605 && args.len() == 1
18606 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18607 {
18608 let Expr::Column(c) = &args[0] else {
18609 unreachable!("guarded above");
18610 };
18611 *e = Expr::Column(crate::ast::ColumnName {
18612 qualifier: Some("EXCLUDED".to_string()),
18613 name: c.name.clone(),
18614 });
18615 }
18616 Expr::FunctionCall { args, .. } => {
18617 for a in args {
18618 Self::rewrite_mysql_values_refs(a);
18619 }
18620 }
18621 Expr::Binary { lhs, rhs, .. } => {
18622 Self::rewrite_mysql_values_refs(lhs);
18623 Self::rewrite_mysql_values_refs(rhs);
18624 }
18625 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18626 Self::rewrite_mysql_values_refs(expr);
18627 }
18628 Expr::Case {
18629 operand,
18630 branches,
18631 else_branch,
18632 } => {
18633 if let Some(op) = operand {
18634 Self::rewrite_mysql_values_refs(op);
18635 }
18636 for (w, t) in branches {
18637 Self::rewrite_mysql_values_refs(w);
18638 Self::rewrite_mysql_values_refs(t);
18639 }
18640 if let Some(el) = else_branch {
18641 Self::rewrite_mysql_values_refs(el);
18642 }
18643 }
18644 _ => {}
18645 }
18646 }
18647
18648 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18649 /// clause sitting between the INSERT body and the trailing
18650 /// RETURNING. All keywords come in as bare idents; `ON` is
18651 /// a reserved Token though.
18652 fn parse_optional_on_conflict(
18653 &mut self,
18654 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18655 if !matches!(self.peek(), Token::On) {
18656 return Ok(None);
18657 }
18658 // Peek further: we want exactly "ON CONFLICT ...". If the
18659 // next ident isn't "conflict", let some other parser handle.
18660 let next_is_conflict = matches!(
18661 self.tokens.get(self.pos + 1),
18662 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18663 );
18664 if !next_is_conflict {
18665 return Ok(None);
18666 }
18667 self.advance(); // ON
18668 self.advance(); // CONFLICT
18669 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18670 // the constraint instead of listing columns (the pg_dump
18671 // form); the engine resolves it.
18672 let mut constraint_name: Option<String> = None;
18673 if matches!(self.peek(), Token::On) {
18674 self.advance(); // ON
18675 match self.advance() {
18676 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18677 }
18678 other => {
18679 return Err(self.err(alloc::format!(
18680 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18681 )));
18682 }
18683 }
18684 constraint_name = Some(self.expect_ident_like()?);
18685 }
18686 // Optional `(col [, col]*)` target list.
18687 let mut target_columns: Vec<String> = Vec::new();
18688 if matches!(self.peek(), Token::LParen) {
18689 self.advance();
18690 loop {
18691 target_columns.push(self.expect_ident_like()?);
18692 match self.peek() {
18693 Token::Comma => {
18694 self.advance();
18695 }
18696 Token::RParen => {
18697 self.advance();
18698 break;
18699 }
18700 other => {
18701 return Err(self.err(alloc::format!(
18702 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18703 )));
18704 }
18705 }
18706 }
18707 }
18708 // v7.39 (round 240) — optional index predicate after the target
18709 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18710 // PARTIAL unique index; SPG's arbiters are full indexes, which
18711 // satisfy any predicate, so it is parsed and carried but not
18712 // consulted (recorded residual: partial-unique-index arbiters).
18713 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18714 self.advance();
18715 Some(self.parse_expr(0)?)
18716 } else {
18717 None
18718 };
18719 // Required `DO`.
18720 match self.advance() {
18721 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18722 other => {
18723 return Err(self.err(alloc::format!(
18724 "expected DO after ON CONFLICT [(…)], got {other:?}"
18725 )));
18726 }
18727 }
18728 // Action: NOTHING | UPDATE SET …
18729 let action = match self.advance() {
18730 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18731 crate::ast::OnConflictAction::Nothing
18732 }
18733 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18734 self.parse_on_conflict_update_action()?
18735 }
18736 other => {
18737 return Err(self.err(alloc::format!(
18738 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18739 )));
18740 }
18741 };
18742 Ok(Some(crate::ast::OnConflictClause {
18743 target_columns,
18744 index_where,
18745 constraint_name,
18746 mysql_lowered: false,
18747 action,
18748 }))
18749 }
18750
18751 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18752 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18753 /// consumed `UPDATE`.
18754 fn parse_on_conflict_update_action(
18755 &mut self,
18756 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18757 // `SET`
18758 match self.advance() {
18759 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18760 other => {
18761 return Err(self.err(alloc::format!(
18762 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18763 )));
18764 }
18765 }
18766 let mut assignments: Vec<(String, Expr)> = Vec::new();
18767 loop {
18768 let col = self.expect_ident_like()?;
18769 if !matches!(self.peek(), Token::Eq) {
18770 return Err(self.err(alloc::format!(
18771 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18772 self.peek()
18773 )));
18774 }
18775 self.advance();
18776 let value = self.parse_expr(0)?;
18777 assignments.push((col, value));
18778 if matches!(self.peek(), Token::Comma) {
18779 self.advance();
18780 continue;
18781 }
18782 break;
18783 }
18784 let where_ = if matches!(self.peek(), Token::Where) {
18785 self.advance();
18786 Some(self.parse_expr(0)?)
18787 } else {
18788 None
18789 };
18790 Ok(crate::ast::OnConflictAction::Update {
18791 assignments,
18792 where_,
18793 })
18794 }
18795
18796 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18797 let mut items = Vec::new();
18798 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18799 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18800 // answers one zero-column row per row of t, and a bare `SELECT`
18801 // answers a single zero-column row. SPG required at least one
18802 // item, so both were syntax errors. Recognised by the token that
18803 // follows — nothing that can start an expression appears here.
18804 if self.select_list_is_empty_here() {
18805 return Ok(items);
18806 }
18807 loop {
18808 items.push(self.parse_select_item()?);
18809 if matches!(self.peek(), Token::Comma) {
18810 self.advance();
18811 } else {
18812 break;
18813 }
18814 }
18815 Ok(items)
18816 }
18817
18818 /// Is the target list empty at this point — i.e. does the next token
18819 /// end the SELECT's item list rather than start an item?
18820 fn select_list_is_empty_here(&self) -> bool {
18821 match self.peek() {
18822 Token::From
18823 | Token::Where
18824 | Token::Group
18825 | Token::Having
18826 | Token::Order
18827 | Token::Limit
18828 | Token::Offset
18829 | Token::Semicolon
18830 | Token::RParen
18831 | Token::Union
18832 | Token::Except
18833 | Token::Eof => true,
18834 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18835 // with unreserved keywords, so they arrive as plain idents.
18836 Token::Ident(s) => {
18837 s.eq_ignore_ascii_case("fetch")
18838 || s.eq_ignore_ascii_case("window")
18839 || s.eq_ignore_ascii_case("intersect")
18840 }
18841 _ => false,
18842 }
18843 }
18844
18845 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18846 if matches!(self.peek(), Token::Star) {
18847 self.advance();
18848 return Ok(SelectItem::Wildcard);
18849 }
18850 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18851 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18852 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18853 // `<ident> . *` with nothing binding tighter.
18854 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18855 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18856 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18857 {
18858 self.advance(); // qualifier
18859 self.advance(); // .
18860 self.advance(); // *
18861 return Ok(SelectItem::QualifiedWildcard(q));
18862 }
18863 }
18864 let start_tok = self.pos;
18865 let expr = self.parse_expr(0)?;
18866 let end_tok = self.consumed_pos();
18867 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18868 // multi-column function returns into columns. Marked here and lowered in
18869 // `parse_bare_select`, where the FROM clause is in hand.
18870 if matches!(self.peek(), Token::Dot)
18871 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18872 {
18873 self.advance(); // .
18874 self.advance(); // *
18875 return Ok(SelectItem::Expr {
18876 expr: Expr::FunctionCall {
18877 name: "__record_expand".to_string(),
18878 args: alloc::vec![expr],
18879 },
18880 alias: None,
18881 });
18882 }
18883 // v7.39.2 — MySQL lets a STRING name a projection item, with or
18884 // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
18885 // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
18886 // `syntax error at or near "'x'"` to all of them.
18887 //
18888 // Only here, not in `parse_optional_alias`: that one also names
18889 // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
18890 // and `FROM t AS 'ta'` are both syntax errors, measured. And only
18891 // after the lexer's own rule has joined adjacent literals, or
18892 // `SELECT 'a' 'b'` would read as a literal aliased `b` where
18893 // MySQL answers the concatenation `ab`.
18894 if self.mysql_dialect {
18895 let at_as = matches!(self.peek(), Token::As)
18896 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
18897 if at_as {
18898 self.advance();
18899 }
18900 if let Token::String(name) = self.peek().clone() {
18901 self.advance();
18902 return Ok(SelectItem::Expr {
18903 expr,
18904 alias: Some(name),
18905 });
18906 }
18907 }
18908 let alias = match self.parse_optional_alias()? {
18909 Some(a) => Some(a),
18910 None => self.mysql_item_label(&expr, start_tok, end_tok),
18911 };
18912 Ok(SelectItem::Expr { expr, alias })
18913 }
18914
18915 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18916 /// carries no `AS`, filled in here so every downstream path reports it
18917 /// without knowing the rule. `None` leaves the item un-aliased, which is
18918 /// what a PG session always gets.
18919 ///
18920 /// Measured against MariaDB 11, three rules and no more:
18921 ///
18922 /// | item | label | why |
18923 /// |------------------|------------|------------------------------|
18924 /// | `lbl.a` | `a` | a column reports its name |
18925 /// | `'it''s'` | `it's` | a string reports its VALUE |
18926 /// | `a + b` | `a + b` | anything else, source text |
18927 ///
18928 /// The third is why this lives in the parser at all: the label is the
18929 /// text the client WROTE, down to the spacing, so it cannot be printed
18930 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18931 ///
18932 /// Comments survive, and that is right: through a `mariadb` CLI both
18933 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18934 /// CLIENT stripping the comment before it sends. Asked over the raw
18935 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18936 /// produces.
18937 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18938 if !self.mysql_dialect {
18939 return None;
18940 }
18941 match expr {
18942 // A column already reports its own name downstream; naming it
18943 // again here would only re-state the qualifier the label drops.
18944 Expr::Column(_) => None,
18945 // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
18946 // `ab`, and MySQL 9.7.2 names the column `a`: the label is
18947 // the first segment as written, not the joined value
18948 // (measured). The lexer logs where it joined them.
18949 Expr::Literal(Literal::String(v)) => Some(
18950 self.merged_first_len(start_tok)
18951 .and_then(|n| v.get(..n))
18952 .map_or_else(|| v.clone(), String::from),
18953 ),
18954 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18955 }
18956 }
18957
18958 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18959 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18960 /// with PG's default column1..columnN names; subsequent rows
18961 /// chain as UNION ALL peers. Shared by the FROM-position
18962 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18963 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18964 let mut row_selects: Vec<SelectStatement> = Vec::new();
18965 loop {
18966 if !matches!(self.peek(), Token::LParen) {
18967 return Err(self.err(alloc::format!(
18968 "expected '(' to start a VALUES row, got {:?}",
18969 self.peek()
18970 )));
18971 }
18972 self.advance(); // (
18973 let mut items: Vec<SelectItem> = Vec::new();
18974 loop {
18975 let expr = self.parse_expr(0)?;
18976 items.push(SelectItem::Expr {
18977 expr,
18978 alias: Some(alloc::format!("column{}", items.len() + 1)),
18979 });
18980 match self.peek() {
18981 Token::Comma => {
18982 self.advance();
18983 }
18984 Token::RParen => break,
18985 other => {
18986 return Err(self.err(alloc::format!(
18987 "expected ',' or ')' in VALUES row, got {other:?}"
18988 )));
18989 }
18990 }
18991 }
18992 self.advance(); // )
18993 row_selects.push(SelectStatement {
18994 locking: None,
18995 ctes: Vec::new(),
18996 distinct: false,
18997 distinct_on: Vec::new(),
18998 items,
18999 from: None,
19000 where_: None,
19001 group_by: None,
19002 group_by_all: false,
19003 having: None,
19004 unions: Vec::new(),
19005 order_by: Vec::new(),
19006 limit: None,
19007 offset: None,
19008 limit_with_ties: false,
19009 window_check_exprs: Vec::new(),
19010 });
19011 if matches!(self.peek(), Token::Comma) {
19012 self.advance();
19013 continue;
19014 }
19015 break;
19016 }
19017 let mut head = row_selects.remove(0);
19018 head.unions = row_selects
19019 .into_iter()
19020 .map(|s| (UnionKind::All, s))
19021 .collect();
19022 Ok(head)
19023 }
19024
19025 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19026 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19027 // children. It was read as a table NAMED `only`, so the query
19028 // failed on `relation "only" does not exist`.
19029 //
19030 // v7.39 (round 644) — and it is no longer a no-op. Round 621
19031 // absorbed the keyword, reasoning that SPG's children are
19032 // separate relations a plain scan does not descend into, so ONLY
19033 // already described the scan. That stopped being true when a
19034 // partition parent started unioning its children: measured,
19035 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19036 // where PG answers 0. The flag is carried now.
19037 let mut only = false;
19038 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19039 && matches!(
19040 self.tokens.get(self.pos + 1),
19041 Some(Token::Ident(_) | Token::QuotedIdent(_))
19042 )
19043 {
19044 only = true;
19045 self.advance();
19046 }
19047 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19048 // for these SRFs the keyword is noise at parse time: the
19049 // join executor already substitutes outer-column references
19050 // into unnest_expr / generate_series_args per outer row
19051 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19052 // licences the correlation even without the keyword. Absorb
19053 // it and fall through to the SRF arms below.
19054 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19055 // just the four builtin SRFs: a user set-returning function on a JOIN's
19056 // right side is the whole point of LATERAL. The keyword stays noise at
19057 // parse time — the join executor substitutes the outer row into the
19058 // call's arguments per outer row.
19059 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19060 && matches!(
19061 self.tokens.get(self.pos + 1),
19062 // The json_each family has its OWN `LATERAL …` arm below, which
19063 // needs to see the keyword — absorbing it here would send those
19064 // calls down the generic table-function channel instead.
19065 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19066 )
19067 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19068 {
19069 self.advance(); // LATERAL
19070 }
19071 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19072 // set-returning function whose argument may reference a
19073 // preceding FROM item. We rewrite this to
19074 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19075 // AS __srf__) AS <alias>` so the existing LATERAL subquery
19076 // executor handles per-outer-row evaluation and the
19077 // SRF-primary jsonb_each_text path handles the inner
19078 // materialisation. Sentori 0067 backfill is the dogfood
19079 // shape.
19080 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19081 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19082 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19083 {
19084 self.advance(); // LATERAL
19085 let each_fn = match self.peek() {
19086 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19087 _ => unreachable!(),
19088 };
19089 self.advance(); // jsonb_each[_text] / json_each[_text]
19090 self.advance(); // (
19091 let arg = self.parse_expr(0)?;
19092 if !matches!(self.peek(), Token::RParen) {
19093 return Err(self.err(alloc::format!(
19094 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19095 self.peek()
19096 )));
19097 }
19098 self.advance();
19099 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19100 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19101 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19102 // FROM jsonb_each_text(<arg>) AS __srf__
19103 // PG's `AS kv(key, value)` column-alias list maps
19104 // positions to names; default to (key, value) when
19105 // omitted (matching the SRF's natural column names).
19106 let srf_alias = "__srf__".to_string();
19107 let key_alias = column_aliases
19108 .first()
19109 .cloned()
19110 .unwrap_or_else(|| "key".to_string());
19111 let value_alias = column_aliases
19112 .get(1)
19113 .cloned()
19114 .unwrap_or_else(|| "value".to_string());
19115 let inner_select = crate::ast::SelectStatement {
19116 locking: None,
19117 ctes: Vec::new(),
19118 distinct: false,
19119 distinct_on: Vec::new(),
19120 items: alloc::vec![
19121 crate::ast::SelectItem::Expr {
19122 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19123 qualifier: Some(srf_alias.clone()),
19124 name: "key".to_string(),
19125 }),
19126 alias: Some(key_alias),
19127 },
19128 crate::ast::SelectItem::Expr {
19129 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19130 qualifier: Some(srf_alias.clone()),
19131 name: "value".to_string(),
19132 }),
19133 alias: Some(value_alias),
19134 },
19135 ],
19136 from: Some(crate::ast::FromClause {
19137 primary: TableRef {
19138 name: srf_alias.clone(),
19139 alias: Some(srf_alias.clone()),
19140 only: false,
19141 as_of_segment: None,
19142 unnest_expr: None,
19143 unnest_column_aliases: Vec::new(),
19144 with_ordinality: false,
19145 generate_series_args: None,
19146 lateral_subquery: None,
19147 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19148 table_fn_call: None,
19149 rows_from: None,
19150 json_table: None,
19151 scalar_fn_item: false,
19152 },
19153 joins: Vec::new(),
19154 }),
19155 where_: None,
19156 group_by: None,
19157 group_by_all: false,
19158 having: None,
19159 unions: Vec::new(),
19160 order_by: Vec::new(),
19161 limit: None,
19162 offset: None,
19163 limit_with_ties: false,
19164 window_check_exprs: Vec::new(),
19165 };
19166 return Ok(TableRef {
19167 name: alias.clone(),
19168 alias: Some(alias),
19169 only: false,
19170 as_of_segment: None,
19171 unnest_expr: None,
19172 unnest_column_aliases: Vec::new(),
19173 with_ordinality: false,
19174 generate_series_args: None,
19175 lateral_subquery: Some(Box::new(inner_select)),
19176 jsonb_each_text_arg: None,
19177 table_fn_call: None,
19178 rows_from: None,
19179 json_table: None,
19180 scalar_fn_item: false,
19181 });
19182 }
19183 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19184 // without an explicit `LATERAL` keyword is the same shape
19185 // PG accepts (SRF naturally licences lateral correlation).
19186 // We mirror the LATERAL rewrite when the argument syntactic-
19187 // ally references an outer column (Column { qualifier:
19188 // Some(_), … }). For simplicity we apply the rewrite
19189 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19190 // in the FROM-list — caller-side join parsing positions
19191 // this peek correctly.
19192 // (Implementation note: detection lives below; the LATERAL
19193 // branch above already covers the explicit form; the bare
19194 // form falls through to the plain SRF arm and the engine
19195 // treats it as a constant-arg SRF if no outer reference is
19196 // present.)
19197 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19198 // table. Detect at the head so it claims precedence over
19199 // every other table-ref shape (unnest / generate_series /
19200 // bare ident); the lateral subquery itself follows the
19201 // regular SELECT grammar.
19202 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19203 // t(cols)`. Each row lowers to a constant SELECT with PG's
19204 // default column1..columnN names; subsequent rows chain as
19205 // UNION ALL peers. The result rides the derived-table
19206 // lateral_subquery channel — zero executor work.
19207 if matches!(self.peek(), Token::LParen)
19208 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19209 {
19210 self.advance(); // (
19211 self.advance(); // VALUES
19212 let head = self.parse_values_rows_body()?;
19213 if !matches!(self.peek(), Token::RParen) {
19214 return Err(self.err(alloc::format!(
19215 "expected ')' after VALUES list, got {:?}",
19216 self.peek()
19217 )));
19218 }
19219 self.advance();
19220 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19221 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19222 return Ok(TableRef {
19223 name,
19224 alias: alias_ident,
19225 only: false,
19226 as_of_segment: None,
19227 unnest_expr: None,
19228 unnest_column_aliases: column_aliases,
19229 with_ordinality: false,
19230 generate_series_args: None,
19231 lateral_subquery: Some(Box::new(head)),
19232 jsonb_each_text_arg: None,
19233 table_fn_call: None,
19234 rows_from: None,
19235 json_table: None,
19236 scalar_fn_item: false,
19237 });
19238 }
19239 // v7.37.17 (17.6 siblings) — plain derived table:
19240 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19241 // lateral_subquery channel the explicit LATERAL form uses —
19242 // an uncorrelated inner SELECT executes identically. The
19243 // inner parse carries UNION tails (they live on
19244 // SelectStatement.unions).
19245 // v7.37 D.20 — the derived-table inner may itself be a
19246 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19247 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19248 // bare `(SELECT …)`. parse_one_statement already routes a leading
19249 // `(` set-op group (its LParen arm) and a leading WITH
19250 // (parse_with_cte_then_select), so widen the second-token gate to
19251 // Select | LParen | WITH.
19252 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19253 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19254 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19255 // has existed since the shorthand landed and `parse_bare_select`
19256 // already routes it ("valid anywhere a SELECT head is"); what was
19257 // missing is this second-token gate, and the CTE body's dispatch
19258 // below. Round 868 found both by putting the shorthand in a
19259 // subquery — the top-level forms had been the only ones tested.
19260 if matches!(self.peek(), Token::LParen)
19261 && (matches!(
19262 self.tokens.get(self.pos + 1),
19263 Some(Token::Select | Token::LParen | Token::Table)
19264 ) || matches!(self.tokens.get(self.pos + 1),
19265 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19266 {
19267 self.advance(); // (
19268 let inner = match self.parse_one_statement()? {
19269 Statement::Select(s) => s,
19270 other => {
19271 return Err(self.err(alloc::format!(
19272 "expected SELECT inside derived table ( … ), got {other:?}"
19273 )));
19274 }
19275 };
19276 if !matches!(self.peek(), Token::RParen) {
19277 return Err(self.err(alloc::format!(
19278 "expected ')' after derived-table subquery, got {:?}",
19279 self.peek()
19280 )));
19281 }
19282 self.advance();
19283 // `AS t(a, b)` column-alias list rides the
19284 // unnest_column_aliases field (same positional-rename
19285 // contract the unnest SRFs use).
19286 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19287 let name = alias_ident
19288 .clone()
19289 .unwrap_or_else(|| "subquery".to_string());
19290 return Ok(TableRef {
19291 name,
19292 alias: alias_ident,
19293 only: false,
19294 as_of_segment: None,
19295 unnest_expr: None,
19296 unnest_column_aliases: column_aliases,
19297 with_ordinality: false,
19298 generate_series_args: None,
19299 lateral_subquery: Some(Box::new(inner)),
19300 jsonb_each_text_arg: None,
19301 table_fn_call: None,
19302 rows_from: None,
19303 json_table: None,
19304 scalar_fn_item: false,
19305 });
19306 }
19307 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19308 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19309 {
19310 self.advance(); // LATERAL
19311 self.advance(); // (
19312 // Parse the inner SELECT.
19313 let inner = match self.parse_one_statement()? {
19314 Statement::Select(s) => s,
19315 other => {
19316 return Err(self.err(alloc::format!(
19317 "expected SELECT inside LATERAL ( … ), got {other:?}"
19318 )));
19319 }
19320 };
19321 if !matches!(self.peek(), Token::RParen) {
19322 return Err(self.err(alloc::format!(
19323 "expected ')' after LATERAL subquery, got {:?}",
19324 self.peek()
19325 )));
19326 }
19327 self.advance();
19328 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19329 // `(VALUES …) t(g)` derived table round-trips through view-body
19330 // Display, which renders on the lateral_subquery channel).
19331 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19332 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19333 return Ok(TableRef {
19334 name,
19335 alias: alias_ident,
19336 only: false,
19337 as_of_segment: None,
19338 unnest_expr: None,
19339 unnest_column_aliases: column_aliases,
19340 with_ordinality: false,
19341 generate_series_args: None,
19342 lateral_subquery: Some(Box::new(inner)),
19343 jsonb_each_text_arg: None,
19344 table_fn_call: None,
19345 rows_from: None,
19346 json_table: None,
19347 scalar_fn_item: false,
19348 });
19349 }
19350 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19351 // function as a FROM item. Emits one row per (key, value)
19352 // pair in the JSONB object argument as TEXT columns. May
19353 // be wrapped in CROSS JOIN LATERAL when the argument
19354 // references a preceding FROM item (sentori migration
19355 // 0067 backfill shape: `CROSS JOIN LATERAL
19356 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19357 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19358 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19359 {
19360 let each_fn = match self.peek() {
19361 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19362 _ => unreachable!(),
19363 };
19364 self.advance(); // jsonb_each[_text] / json_each[_text]
19365 self.advance(); // (
19366 let arg = self.parse_expr(0)?;
19367 if !matches!(self.peek(), Token::RParen) {
19368 return Err(self.err(alloc::format!(
19369 "expected ')' after {each_fn}() argument, got {:?}",
19370 self.peek()
19371 )));
19372 }
19373 self.advance();
19374 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19375 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19376 return Ok(TableRef {
19377 name,
19378 alias: alias_ident,
19379 only: false,
19380 as_of_segment: None,
19381 unnest_expr: None,
19382 // `AS t(k, v)` renames key/value positionally, same as the
19383 // LATERAL-position form already does.
19384 unnest_column_aliases: column_aliases,
19385 with_ordinality: false,
19386 generate_series_args: None,
19387 lateral_subquery: None,
19388 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19389 table_fn_call: None,
19390 rows_from: None,
19391 json_table: None,
19392 scalar_fn_item: false,
19393 });
19394 }
19395 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19396 // (+ json_ variants) — record-returning JSON functions with a
19397 // column-definition list. Desugar to a derived table that
19398 // projects each declared column from the JSON via `->>` + a cast,
19399 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19400 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19401 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19402 {
19403 return self.parse_json_to_record_from();
19404 }
19405 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19406 // row is a text[] of capture groups, so it cannot desugar to unnest
19407 // (that would flatten the array). Wrap it as a derived table
19408 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19409 // SRF path already emits one text[] row per match. PG names the column
19410 // `regexp_matches`; an `AS a(col)` alias overrides it.
19411 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19412 if s.eq_ignore_ascii_case("regexp_matches"))
19413 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19414 {
19415 self.advance(); // fn name
19416 self.advance(); // (
19417 let mut fn_args: Vec<Expr> = Vec::new();
19418 loop {
19419 fn_args.push(self.parse_expr(0)?);
19420 if matches!(self.peek(), Token::Comma) {
19421 self.advance();
19422 continue;
19423 }
19424 break;
19425 }
19426 if !matches!(self.peek(), Token::RParen) {
19427 return Err(self.err(alloc::format!(
19428 "expected ')' after regexp_matches() arguments, got {:?}",
19429 self.peek()
19430 )));
19431 }
19432 self.advance();
19433 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19434 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19435 // it, so it died on the `with` token while every other table function
19436 // accepted it.
19437 let with_ordinality = self.absorb_with_ordinality();
19438 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19439 let table_alias = alias_ident
19440 .clone()
19441 .unwrap_or_else(|| "regexp_matches".to_string());
19442 // PG names a single-column function's output column after the ALIAS
19443 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19444 // `m` reads as that column and not as a whole-row composite. Naming
19445 // it after the function regardless made `SELECT m[1] FROM … AS m`
19446 // subscript a record.
19447 let col_name = column_aliases
19448 .first()
19449 .cloned()
19450 .or_else(|| alias_ident.clone())
19451 .unwrap_or_else(|| "regexp_matches".to_string());
19452 let inner = crate::ast::SelectStatement {
19453 locking: None,
19454 ctes: Vec::new(),
19455 distinct: false,
19456 distinct_on: Vec::new(),
19457 items: alloc::vec![SelectItem::Expr {
19458 expr: Expr::FunctionCall {
19459 name: "regexp_matches".to_string(),
19460 args: fn_args,
19461 },
19462 alias: Some(col_name),
19463 }],
19464 from: None,
19465 where_: None,
19466 group_by: None,
19467 group_by_all: false,
19468 having: None,
19469 unions: Vec::new(),
19470 order_by: Vec::new(),
19471 limit: None,
19472 offset: None,
19473 limit_with_ties: false,
19474 window_check_exprs: Vec::new(),
19475 };
19476 return Ok(TableRef {
19477 name: table_alias.clone(),
19478 alias: Some(table_alias),
19479 only: false,
19480 as_of_segment: None,
19481 unnest_expr: None,
19482 unnest_column_aliases: column_aliases,
19483 with_ordinality,
19484 generate_series_args: None,
19485 lateral_subquery: Some(Box::new(inner)),
19486 jsonb_each_text_arg: None,
19487 table_fn_call: None,
19488 rows_from: None,
19489 json_table: None,
19490 // regexp_matches returns text[], a base type: `SELECT m FROM
19491 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19492 scalar_fn_item: true,
19493 });
19494 }
19495 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19496 // / json_ variants as a FROM item. Rewritten into
19497 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19498 // elements as a TEXT array, and the existing unnest SRF path
19499 // materialises one row per element. PG's natural column name
19500 // is `value`; an `AS a(col)` column-alias list overrides it.
19501 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19502 if s.eq_ignore_ascii_case("jsonb_array_elements")
19503 || s.eq_ignore_ascii_case("json_array_elements")
19504 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19505 || s.eq_ignore_ascii_case("json_array_elements_text")
19506 || s.eq_ignore_ascii_case("jsonb_object_keys")
19507 || s.eq_ignore_ascii_case("json_object_keys")
19508 || s.eq_ignore_ascii_case("jsonb_path_query")
19509 || s.eq_ignore_ascii_case("json_path_query")
19510 || s.eq_ignore_ascii_case("generate_subscripts")
19511 || s.eq_ignore_ascii_case("string_to_table")
19512 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19513 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19514 {
19515 let fn_name = match self.peek() {
19516 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19517 _ => unreachable!(),
19518 };
19519 self.advance(); // fn name
19520 self.advance(); // (
19521 let mut fn_args: Vec<Expr> = Vec::new();
19522 loop {
19523 fn_args.push(self.parse_expr(0)?);
19524 if matches!(self.peek(), Token::Comma) {
19525 self.advance();
19526 continue;
19527 }
19528 break;
19529 }
19530 if !matches!(self.peek(), Token::RParen) {
19531 return Err(self.err(alloc::format!(
19532 "expected ')' after {fn_name}() arguments, got {:?}",
19533 self.peek()
19534 )));
19535 }
19536 self.advance();
19537 let with_ordinality = self.absorb_with_ordinality();
19538 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19539 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19540 // PG's natural column name: the array-elements SRFs
19541 // declare an OUT parameter `value`; jsonb_object_keys
19542 // and generate_subscripts have none, so the column is
19543 // named after the function. A bare table alias on a
19544 // single-column SRF renames the column too (PG: `FROM
19545 // generate_subscripts(a, 1) AS s` projects column s) —
19546 // except for the OUT-parameter SRFs, whose column stays
19547 // `value` under a bare alias.
19548 let natural_col = if fn_name.ends_with("_array_elements")
19549 || fn_name.ends_with("_array_elements_text")
19550 {
19551 "value".to_string()
19552 } else {
19553 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19554 };
19555 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19556 // Keep any further entries — the second names the
19557 // ordinality column under WITH ORDINALITY.
19558 srf_cols.extend(column_aliases.into_iter().skip(1));
19559 // The *_to_table SRFs are row-streams over the existing
19560 // *_to_array scalars — map the call target; the display
19561 // name (alias / column defaults) keeps the SRF spelling.
19562 let call_name = match fn_name.as_str() {
19563 "string_to_table" => "string_to_array".to_string(),
19564 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19565 _ => fn_name,
19566 };
19567 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19568 // preceding FROM item (bare or qualified column) is correlated;
19569 // route it through the per-outer-row lateral channel.
19570 let expr = crate::ast::Expr::FunctionCall {
19571 name: call_name,
19572 args: fn_args,
19573 };
19574 let correlated = Self::expr_has_any_column(&expr);
19575 let tref = TableRef {
19576 name,
19577 alias: alias_ident,
19578 only: false,
19579 as_of_segment: None,
19580 unnest_expr: Some(Box::new(expr)),
19581 unnest_column_aliases: srf_cols,
19582 with_ordinality,
19583 generate_series_args: None,
19584 lateral_subquery: None,
19585 jsonb_each_text_arg: None,
19586 table_fn_call: None,
19587 rows_from: None,
19588 json_table: None,
19589 // Each of these returns a BASE type (jsonb / text / int), so the item's
19590 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19591 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19592 scalar_fn_item: !with_ordinality,
19593 };
19594 return Ok(if correlated {
19595 Self::wrap_correlated_srf(tref)
19596 } else {
19597 tref
19598 });
19599 }
19600 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19601 // explicit parallel-zip syntax. Each entry lowers to its
19602 // array-returning scalar form (unnest(x) → x itself; the
19603 // FROM-SRF rewrite family → their scalar array calls) and
19604 // the list rides the multi-arg unnest zip channel:
19605 // NULL-padded to the longest, WITH ORDINALITY appends the
19606 // counter. generate_series has no scalar array form and
19607 // errors honestly.
19608 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19609 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19610 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19611 {
19612 self.advance(); // ROWS
19613 self.advance(); // FROM
19614 self.advance(); // (
19615 let mut entries: Vec<Expr> = Vec::new();
19616 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19617 // Used only when some entry has no array form.
19618 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19619 loop {
19620 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19621 if !matches!(self.peek(), Token::LParen) {
19622 return Err(self.err(alloc::format!(
19623 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19624 self.peek()
19625 )));
19626 }
19627 self.advance();
19628 let mut fn_args: Vec<Expr> = Vec::new();
19629 if !matches!(self.peek(), Token::RParen) {
19630 loop {
19631 fn_args.push(self.parse_expr(0)?);
19632 if matches!(self.peek(), Token::Comma) {
19633 self.advance();
19634 continue;
19635 }
19636 break;
19637 }
19638 }
19639 if !matches!(self.peek(), Token::RParen) {
19640 return Err(self.err(alloc::format!(
19641 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19642 self.peek()
19643 )));
19644 }
19645 self.advance();
19646 let entry = match fn_name.as_str() {
19647 "unnest" => {
19648 if fn_args.len() != 1 {
19649 return Err(
19650 self.err("unnest inside ROWS FROM takes exactly one array".into())
19651 );
19652 }
19653 fn_args.pop().expect("len checked")
19654 }
19655 "jsonb_array_elements"
19656 | "json_array_elements"
19657 | "jsonb_array_elements_text"
19658 | "json_array_elements_text"
19659 | "jsonb_object_keys"
19660 | "json_object_keys"
19661 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19662 name: fn_name,
19663 args: fn_args,
19664 },
19665 "string_to_table" => crate::ast::Expr::FunctionCall {
19666 name: "string_to_array".to_string(),
19667 args: fn_args,
19668 },
19669 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19670 name: "regexp_split_to_array".to_string(),
19671 args: fn_args,
19672 },
19673 // v7.39 (read01 round 74) — an SRF with no array form
19674 // (`generate_series`, a user `RETURNS SETOF` function) has no
19675 // scalar expression to zip, so the WHOLE list switches to the
19676 // rows_from channel, which runs each function and zips the
19677 // rows themselves. The all-array case keeps the old lowering:
19678 // it is well-trodden and this must not disturb it.
19679 _ => {
19680 generic.push((fn_name, fn_args));
19681 if matches!(self.peek(), Token::Comma) {
19682 self.advance();
19683 continue;
19684 }
19685 break;
19686 }
19687 };
19688 generic.push((
19689 // The array-able entries carry their lowered expr along, so a
19690 // MIXED list still works: the engine sees the scalar array
19691 // form and unnests it.
19692 "__array".to_string(),
19693 alloc::vec![entry.clone()],
19694 ));
19695 entries.push(entry);
19696 if matches!(self.peek(), Token::Comma) {
19697 self.advance();
19698 continue;
19699 }
19700 break;
19701 }
19702 if !matches!(self.peek(), Token::RParen) {
19703 return Err(self.err(alloc::format!(
19704 "expected ')' to close ROWS FROM, got {:?}",
19705 self.peek()
19706 )));
19707 }
19708 self.advance();
19709 let with_ordinality = self.absorb_with_ordinality();
19710 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19711 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19712 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19713 // list rides the generic channel.
19714 if generic.iter().any(|(n, _)| n != "__array") {
19715 let correlated = generic
19716 .iter()
19717 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19718 let tref = TableRef {
19719 name,
19720 alias: alias_ident,
19721 only: false,
19722 as_of_segment: None,
19723 unnest_expr: None,
19724 unnest_column_aliases,
19725 with_ordinality,
19726 generate_series_args: None,
19727 lateral_subquery: None,
19728 jsonb_each_text_arg: None,
19729 table_fn_call: None,
19730 rows_from: Some(generic),
19731 json_table: None,
19732 scalar_fn_item: false,
19733 };
19734 return Ok(if correlated {
19735 Self::wrap_correlated_srf(tref)
19736 } else {
19737 tref
19738 });
19739 }
19740 let correlated = entries.iter().any(Self::expr_has_any_column);
19741 let expr = if entries.len() == 1 {
19742 entries.pop().expect("len checked")
19743 } else {
19744 crate::ast::Expr::FunctionCall {
19745 name: "__unnest_zip".to_string(),
19746 args: entries,
19747 }
19748 };
19749 let tref = TableRef {
19750 name,
19751 alias: alias_ident,
19752 only: false,
19753 as_of_segment: None,
19754 unnest_expr: Some(Box::new(expr)),
19755 unnest_column_aliases,
19756 with_ordinality,
19757 generate_series_args: None,
19758 lateral_subquery: None,
19759 jsonb_each_text_arg: None,
19760 table_fn_call: None,
19761 rows_from: None,
19762 json_table: None,
19763 scalar_fn_item: false,
19764 };
19765 return Ok(if correlated {
19766 Self::wrap_correlated_srf(tref)
19767 } else {
19768 tref
19769 });
19770 }
19771 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19772 // source. Detect at the head before the bare-ident fallback;
19773 // unnest is not a reserved token.
19774 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19775 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19776 {
19777 self.advance(); // unnest
19778 self.advance(); // (
19779 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19780 while matches!(self.peek(), Token::Comma) {
19781 self.advance();
19782 srf_args.push(self.parse_expr(0)?);
19783 }
19784 if !matches!(self.peek(), Token::RParen) {
19785 return Err(self.err(alloc::format!(
19786 "expected ')' after unnest() argument, got {:?}",
19787 self.peek()
19788 )));
19789 }
19790 self.advance();
19791 // Multi-arg unnest(a, b, …) zips the arrays in
19792 // parallel, NULL-padding to the longest (PG's ROWS
19793 // FROM shorthand). Lower onto the unnest channel as an
19794 // internal marker call the executors unpack.
19795 let expr = if srf_args.len() == 1 {
19796 srf_args.pop().expect("len checked")
19797 } else {
19798 crate::ast::Expr::FunctionCall {
19799 name: "__unnest_zip".to_string(),
19800 args: srf_args,
19801 }
19802 };
19803 let with_ordinality = self.absorb_with_ordinality();
19804 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19805 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19806 let correlated = Self::expr_has_any_column(&expr);
19807 let tref = TableRef {
19808 name,
19809 alias: alias_ident,
19810 only: false,
19811 as_of_segment: None,
19812 unnest_expr: Some(Box::new(expr)),
19813 unnest_column_aliases,
19814 with_ordinality,
19815 generate_series_args: None,
19816 lateral_subquery: None,
19817 jsonb_each_text_arg: None,
19818 table_fn_call: None,
19819 rows_from: None,
19820 json_table: None,
19821 scalar_fn_item: false,
19822 };
19823 return Ok(if correlated {
19824 Self::wrap_correlated_srf(tref)
19825 } else {
19826 tref
19827 });
19828 }
19829 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19830 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19831 // generic table-fn arg parser can't read), so it is intercepted
19832 // here BEFORE the generic dispatch. The doc expr may reference
19833 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19834 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19835 if s.eq_ignore_ascii_case("json_table"))
19836 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19837 {
19838 let tref = self.parse_json_table_ref()?;
19839 let correlated = tref
19840 .json_table
19841 .as_deref()
19842 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19843 return Ok(if correlated {
19844 Self::wrap_correlated_srf(tref)
19845 } else {
19846 tref
19847 });
19848 }
19849 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19850 // functions dispatched by name (`pg_partition_tree('t')`,
19851 // `pg_partition_ancestors('t')`). Same head-detection shape as
19852 // unnest; the engine executor owns the row shape per function.
19853 // v7.39 (read01 round 65) — and a USER function in FROM position
19854 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19855 // (generate_series / unnest / the json_each family) keep it — their arms
19856 // sit further down, so they are excluded here by name rather than by
19857 // ordering. Anything else that is an ident followed by `(` is a table
19858 // function; the engine executor decides whether it is a builtin, a
19859 // set-returning user function, or an error.
19860 // 7.38.1 S5.1 — pg_dump spells its table functions
19861 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19862 // strip the pg_catalog prefix here so the same head-detection
19863 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19864 // meaning.
19865 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19866 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19867 && matches!(
19868 self.tokens.get(self.pos + 2),
19869 Some(Token::Ident(_) | Token::QuotedIdent(_))
19870 )
19871 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19872 {
19873 self.advance(); // pg_catalog
19874 self.advance(); // .
19875 }
19876 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19877 if !s.eq_ignore_ascii_case("generate_series")
19878 && !s.eq_ignore_ascii_case("unnest")
19879 && !is_json_each_name(s))
19880 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19881 {
19882 // Body out-of-line — this parse sits on the FROM/subquery
19883 // recursion chain (debug frame-cliff discipline).
19884 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19885 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19886 // outer row, so it rides the lateral channel. Same rule the unnest
19887 // arm uses.
19888 let tref = self.parse_table_fn_ref()?;
19889 let correlated = tref
19890 .table_fn_call
19891 .as_deref()
19892 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19893 return Ok(if correlated {
19894 Self::wrap_correlated_srf(tref)
19895 } else {
19896 tref
19897 });
19898 }
19899 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19900 // [, step])` set-returning source. Same shape as unnest:
19901 // detect at the head, parse the comma-separated arg list,
19902 // dispatch downstream through the engine's set-returning
19903 // path. Supports integer triplets (mailrs's `WITH row_no AS
19904 // (SELECT * FROM generate_series(1, N))` pattern) and
19905 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19906 // date-range iteration pattern, which pre-3.10 had no
19907 // direct equivalent in SPG).
19908 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19909 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19910 {
19911 self.advance(); // generate_series
19912 self.advance(); // (
19913 let mut args: Vec<Expr> = Vec::new();
19914 loop {
19915 args.push(self.parse_expr(0)?);
19916 if matches!(self.peek(), Token::Comma) {
19917 self.advance();
19918 continue;
19919 }
19920 break;
19921 }
19922 if !matches!(self.peek(), Token::RParen) {
19923 return Err(self.err(alloc::format!(
19924 "expected ')' after generate_series() arguments, got {:?}",
19925 self.peek()
19926 )));
19927 }
19928 self.advance();
19929 if args.len() < 2 || args.len() > 3 {
19930 return Err(self.err(alloc::format!(
19931 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19932 args.len()
19933 )));
19934 }
19935 let with_ordinality = self.absorb_with_ordinality();
19936 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19937 let name = alias_ident
19938 .clone()
19939 .unwrap_or_else(|| "generate_series".to_string());
19940 let correlated = args.iter().any(Self::expr_has_any_column);
19941 let tref = TableRef {
19942 name,
19943 alias: alias_ident,
19944 only: false,
19945 as_of_segment: None,
19946 unnest_expr: None,
19947 unnest_column_aliases: column_aliases,
19948 with_ordinality,
19949 generate_series_args: Some(args),
19950 lateral_subquery: None,
19951 jsonb_each_text_arg: None,
19952 table_fn_call: None,
19953 rows_from: None,
19954 json_table: None,
19955 scalar_fn_item: false,
19956 };
19957 return Ok(if correlated {
19958 Self::wrap_correlated_srf(tref)
19959 } else {
19960 tref
19961 });
19962 }
19963 // v7.16.2 — preserve information_schema / pg_catalog
19964 // qualifiers (mailrs round-10 A.3). The generic
19965 // `expect_ident_like` strip silently drops the schema;
19966 // we want the engine to recognise these PG meta tables
19967 // and synthesise rows from the live catalog. Produce a
19968 // synthetic name (`__spg_info_columns` etc.) so the
19969 // engine's SELECT-side router can dispatch without
19970 // clashing with any user-defined `columns` table.
19971 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19972 (synth, Some(orig))
19973 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19974 (synth, Some(orig))
19975 } else {
19976 (self.expect_ident_like()?, None)
19977 };
19978 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19979 // time-travel clause. Parse BEFORE the alias so the
19980 // alias can still ride at the tail (`tbl AS OF SEGMENT
19981 // '5' alias`). `AS` is a reserved keyword token, while
19982 // `OF` and `SEGMENT` are bare idents.
19983 let as_of_segment = if matches!(self.peek(), Token::As)
19984 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19985 {
19986 self.advance(); // AS
19987 self.advance(); // OF
19988 let kw = match self.peek().clone() {
19989 Token::Ident(s) | Token::QuotedIdent(s) => s,
19990 other => {
19991 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19992 }
19993 };
19994 if !kw.eq_ignore_ascii_case("segment") {
19995 return Err(self.err(format!(
19996 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19997 )));
19998 }
19999 self.advance();
20000 // Segment id literal — accept either a string or
20001 // integer for operator ergonomics.
20002 let id = match self.advance() {
20003 Token::String(s) => s
20004 .parse::<u32>()
20005 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20006 Token::Integer(n) => u32::try_from(n)
20007 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20008 other => {
20009 return Err(self.err(format!(
20010 "expected segment id literal after AS OF SEGMENT, got {other:?}"
20011 )));
20012 }
20013 };
20014 Some(id)
20015 } else {
20016 None
20017 };
20018 // TABLESAMPLE is not a reserved token — keep the bare-ident
20019 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20020 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20021 {
20022 None
20023 } else {
20024 self.parse_optional_alias()?
20025 };
20026 // r1052 — a catalog name rewritten to its synthetic form keeps
20027 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20028 // still binds after `pg_cast` became `__spg_pg_cast`. PG
20029 // semantics: the visible name of `pg_catalog.pg_cast` IS
20030 // `pg_cast`. Without this, every table-name-qualified column
20031 // on a synthesised catalog answered "missing FROM-clause
20032 // entry" — which is the wall pg_dump hit on its first
20033 // pg_proc/pg_cast query.
20034 let alias = match (&alias, &meta_original) {
20035 (None, Some(orig)) if *orig != name => Some(orig.clone()),
20036 _ => alias,
20037 };
20038 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20039 // (PG grammar). BERNOULLI lowers to a per-row
20040 // `random() < p/100` conjunct on the enclosing SELECT's
20041 // WHERE — exact row-level Bernoulli semantics. SYSTEM
20042 // shares the lowering: SPG has no page structure to
20043 // sample, and the row-level form returns the same expected
20044 // fraction. REPEATABLE(seed) promises a deterministic
20045 // sample SPG cannot honour yet — honest error rather than
20046 // a silently ignored seed.
20047 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20048 self.advance();
20049 let method = self.expect_ident_like()?;
20050 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20051 return Err(self.err(alloc::format!(
20052 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20053 )));
20054 }
20055 if !matches!(self.peek(), Token::LParen) {
20056 return Err(self.err(alloc::format!(
20057 "expected '(' after TABLESAMPLE {}, got {:?}",
20058 method.to_ascii_uppercase(),
20059 self.peek()
20060 )));
20061 }
20062 self.advance();
20063 let percent = self.parse_expr(0)?;
20064 if !matches!(self.peek(), Token::RParen) {
20065 return Err(self.err(alloc::format!(
20066 "expected ')' after TABLESAMPLE percentage, got {:?}",
20067 self.peek()
20068 )));
20069 }
20070 self.advance();
20071 // REPEATABLE(seed) → a deterministic per-row draw seeded by
20072 // `seed`, so the sample is stable across repeats and rescans.
20073 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20074 let mut sample_seed: Option<Expr> = None;
20075 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20076 self.advance();
20077 if !matches!(self.peek(), Token::LParen) {
20078 return Err(self.err(alloc::format!(
20079 "expected '(' after REPEATABLE, got {:?}",
20080 self.peek()
20081 )));
20082 }
20083 self.advance();
20084 let seed = self.parse_expr(0)?;
20085 if !matches!(self.peek(), Token::RParen) {
20086 return Err(self.err(alloc::format!(
20087 "expected ')' after REPEATABLE seed, got {:?}",
20088 self.peek()
20089 )));
20090 }
20091 self.advance();
20092 sample_seed = Some(seed);
20093 }
20094 let draw = match sample_seed {
20095 Some(seed) => Expr::FunctionCall {
20096 name: "__tsm_fract".to_string(),
20097 args: alloc::vec![seed],
20098 },
20099 None => Expr::FunctionCall {
20100 name: "random".to_string(),
20101 args: Vec::new(),
20102 },
20103 };
20104 self.pending_sample_preds.push(Expr::Binary {
20105 lhs: Box::new(draw),
20106 op: crate::ast::BinOp::Lt,
20107 rhs: Box::new(Expr::Binary {
20108 lhs: Box::new(percent),
20109 op: crate::ast::BinOp::Div,
20110 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20111 }),
20112 });
20113 }
20114 Ok(TableRef {
20115 name,
20116 alias,
20117 only,
20118 as_of_segment,
20119 unnest_expr: None,
20120 unnest_column_aliases: Vec::new(),
20121 with_ordinality: false,
20122 generate_series_args: None,
20123 lateral_subquery: None,
20124 jsonb_each_text_arg: None,
20125 table_fn_call: None,
20126 rows_from: None,
20127 json_table: None,
20128 scalar_fn_item: false,
20129 })
20130 }
20131
20132 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20133 /// but also accepts `AS alias(col [, col, …])` — the
20134 /// PG-standard table-function column-list form. The column
20135 /// list is only honoured when paired with `UNNEST(...)` in
20136 /// the parent; other call sites currently discard it.
20137 /// True when the expression tree contains a qualified column
20138 /// reference (`t.col`) — the syntactic marker that an SRF
20139 /// argument correlates with a preceding FROM item.
20140 fn expr_has_qualified_column(e: &Expr) -> bool {
20141 match e {
20142 Expr::Column(c) => c.qualifier.is_some(),
20143 Expr::Binary { lhs, rhs, .. } => {
20144 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20145 }
20146 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20147 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20148 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20149 Expr::Case {
20150 operand,
20151 branches,
20152 else_branch,
20153 } => {
20154 operand
20155 .as_deref()
20156 .is_some_and(Self::expr_has_qualified_column)
20157 || branches.iter().any(|(w, t)| {
20158 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20159 })
20160 || else_branch
20161 .as_deref()
20162 .is_some_and(Self::expr_has_qualified_column)
20163 }
20164 _ => false,
20165 }
20166 }
20167
20168 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20169 /// counts a bare (unqualified) column. A set-returning function has no
20170 /// input columns of its own, so ANY column in its arguments is an outer
20171 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20172 fn expr_has_any_column(e: &Expr) -> bool {
20173 match e {
20174 Expr::Column(_) => true,
20175 Expr::Binary { lhs, rhs, .. } => {
20176 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20177 }
20178 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20179 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20180 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20181 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20182 // constructor or subscript fell to the `_ => false` arm, so
20183 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20184 // channel and the eager peer eval answered `column "x" does
20185 // not exist` (the substitution walker already recurses both
20186 // shapes; only this detector was blind to them).
20187 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20188 Expr::ArraySubscript { target, index } => {
20189 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20190 }
20191 Expr::Case {
20192 operand,
20193 branches,
20194 else_branch,
20195 } => {
20196 operand.as_deref().is_some_and(Self::expr_has_any_column)
20197 || branches
20198 .iter()
20199 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20200 || else_branch
20201 .as_deref()
20202 .is_some_and(Self::expr_has_any_column)
20203 }
20204 _ => false,
20205 }
20206 }
20207
20208 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20209 /// `generate_series(1, t.n)`) into the lateral_subquery
20210 /// channel: `SELECT * FROM <srf>` executes per outer row with
20211 /// outer references substituted (v7.37.43-T4.5 machinery).
20212 /// Uncorrelated SRFs stay on their plain channels.
20213 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20214 let name = srf.name.clone();
20215 let alias = srf.alias.clone();
20216 let inner = crate::ast::SelectStatement {
20217 locking: None,
20218 ctes: Vec::new(),
20219 distinct: false,
20220 distinct_on: Vec::new(),
20221 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20222 from: Some(crate::ast::FromClause {
20223 primary: srf,
20224 joins: Vec::new(),
20225 }),
20226 where_: None,
20227 group_by: None,
20228 group_by_all: false,
20229 having: None,
20230 unions: Vec::new(),
20231 order_by: Vec::new(),
20232 limit: None,
20233 offset: None,
20234 limit_with_ties: false,
20235 window_check_exprs: Vec::new(),
20236 };
20237 TableRef {
20238 name,
20239 alias,
20240 only: false,
20241 as_of_segment: None,
20242 unnest_expr: None,
20243 unnest_column_aliases: Vec::new(),
20244 with_ordinality: false,
20245 generate_series_args: None,
20246 lateral_subquery: Some(Box::new(inner)),
20247 jsonb_each_text_arg: None,
20248 table_fn_call: None,
20249 rows_from: None,
20250 json_table: None,
20251 scalar_fn_item: false,
20252 }
20253 }
20254
20255 /// True when the expression tree contains an unresolved
20256 /// `OVER w` marker (see parse_over_clause).
20257 fn expr_has_named_window(e: &Expr) -> bool {
20258 match e {
20259 Expr::WindowFunction { partition_by, .. } => matches!(
20260 partition_by.as_slice(),
20261 [Expr::Column(c)] if matches!(
20262 c.qualifier.as_deref(),
20263 Some("__named_window__") | Some("__named_window_ref__")
20264 )
20265 ),
20266 Expr::Binary { lhs, rhs, .. } => {
20267 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20268 }
20269 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20270 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20271 Expr::Case {
20272 operand,
20273 branches,
20274 else_branch,
20275 } => {
20276 operand.as_deref().is_some_and(Self::expr_has_named_window)
20277 || branches.iter().any(|(w, t)| {
20278 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20279 })
20280 || else_branch
20281 .as_deref()
20282 .is_some_and(Self::expr_has_named_window)
20283 }
20284 _ => false,
20285 }
20286 }
20287
20288 /// v7.39 (round 705) — the NAMES the expression references through the
20289 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20290 /// definitions nothing referenced. Traversal mirrors
20291 /// `expr_has_named_window` above.
20292 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20293 match e {
20294 Expr::WindowFunction { partition_by, .. } => {
20295 if let [Expr::Column(c)] = partition_by.as_slice()
20296 && matches!(
20297 c.qualifier.as_deref(),
20298 Some("__named_window__") | Some("__named_window_ref__")
20299 )
20300 {
20301 into.push(c.name.clone());
20302 }
20303 }
20304 Expr::Binary { lhs, rhs, .. } => {
20305 Self::collect_named_window_refs(lhs, into);
20306 Self::collect_named_window_refs(rhs, into);
20307 }
20308 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20309 Self::collect_named_window_refs(expr, into);
20310 }
20311 Expr::FunctionCall { args, .. } => {
20312 for a in args {
20313 Self::collect_named_window_refs(a, into);
20314 }
20315 }
20316 Expr::Case {
20317 operand,
20318 branches,
20319 else_branch,
20320 } => {
20321 if let Some(o) = operand.as_deref() {
20322 Self::collect_named_window_refs(o, into);
20323 }
20324 for (w, t) in branches {
20325 Self::collect_named_window_refs(w, into);
20326 Self::collect_named_window_refs(t, into);
20327 }
20328 if let Some(eb) = else_branch.as_deref() {
20329 Self::collect_named_window_refs(eb, into);
20330 }
20331 }
20332 _ => {}
20333 }
20334 }
20335
20336 /// Inline named-window definitions into the `OVER w` markers.
20337 /// An unknown name errors (PG: window "w" does not exist).
20338 #[allow(clippy::type_complexity)]
20339 fn substitute_named_windows(
20340 e: &mut Expr,
20341 defs: &[(
20342 String,
20343 (
20344 Vec<Expr>,
20345 Vec<(Expr, bool, Option<bool>)>,
20346 Option<WindowFrame>,
20347 ),
20348 )],
20349 ) -> Result<(), String> {
20350 match e {
20351 Expr::WindowFunction {
20352 partition_by,
20353 order_by,
20354 frame,
20355 ..
20356 } => {
20357 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20358 // from the bare `OVER w1` (a plain reference).
20359 let named = match partition_by.as_slice() {
20360 [Expr::Column(c)] => match c.qualifier.as_deref() {
20361 Some("__named_window__") => Some((c.name.clone(), false)),
20362 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20363 _ => None,
20364 },
20365 _ => None,
20366 };
20367 if let Some((wname, is_copy)) = named {
20368 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20369 else {
20370 return Err(alloc::format!("window {wname:?} does not exist"));
20371 };
20372 if !is_copy {
20373 *partition_by = def.0.clone();
20374 *order_by = def.1.clone();
20375 *frame = def.2.clone();
20376 return Ok(());
20377 }
20378 // v7.39 (round 229) — PG's copy rules, probed against
20379 // 18.4: a copy inherits the partitioning, may supply an
20380 // ordering only when the base has none, and may not copy
20381 // a base that already carries a frame (its own frame
20382 // would be ambiguous with the inherited one).
20383 if !def.1.is_empty() && !order_by.is_empty() {
20384 return Err(alloc::format!(
20385 "cannot override ORDER BY clause of window \"{wname}\""
20386 ));
20387 }
20388 if def.2.is_some() {
20389 return Err(alloc::format!(
20390 "cannot copy window \"{wname}\" because it has a frame clause"
20391 ));
20392 }
20393 *partition_by = def.0.clone();
20394 if order_by.is_empty() {
20395 *order_by = def.1.clone();
20396 }
20397 }
20398 Ok(())
20399 }
20400 Expr::Binary { lhs, rhs, .. } => {
20401 Self::substitute_named_windows(lhs, defs)?;
20402 Self::substitute_named_windows(rhs, defs)
20403 }
20404 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20405 Self::substitute_named_windows(expr, defs)
20406 }
20407 Expr::FunctionCall { args, .. } => {
20408 for a in args {
20409 Self::substitute_named_windows(a, defs)?;
20410 }
20411 Ok(())
20412 }
20413 Expr::Case {
20414 operand,
20415 branches,
20416 else_branch,
20417 } => {
20418 if let Some(op) = operand {
20419 Self::substitute_named_windows(op, defs)?;
20420 }
20421 for (w, t) in branches {
20422 Self::substitute_named_windows(w, defs)?;
20423 Self::substitute_named_windows(t, defs)?;
20424 }
20425 if let Some(el) = else_branch {
20426 Self::substitute_named_windows(el, defs)?;
20427 }
20428 Ok(())
20429 }
20430 _ => Ok(()),
20431 }
20432 }
20433
20434 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20435 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20436 /// composition.
20437 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20438 debug_assert!(matches!(self.peek(), Token::Table));
20439 self.advance(); // TABLE
20440 let tname = self.expect_ident_like()?;
20441 Ok(SelectStatement {
20442 locking: None,
20443 ctes: Vec::new(),
20444 distinct: false,
20445 distinct_on: Vec::new(),
20446 items: alloc::vec![SelectItem::Wildcard],
20447 from: Some(FromClause {
20448 primary: TableRef {
20449 name: tname,
20450 alias: None,
20451 only: false,
20452 as_of_segment: None,
20453 unnest_expr: None,
20454 unnest_column_aliases: Vec::new(),
20455 with_ordinality: false,
20456 generate_series_args: None,
20457 lateral_subquery: None,
20458 jsonb_each_text_arg: None,
20459 table_fn_call: None,
20460 rows_from: None,
20461 json_table: None,
20462 scalar_fn_item: false,
20463 },
20464 joins: Vec::new(),
20465 }),
20466 where_: None,
20467 group_by: None,
20468 group_by_all: false,
20469 having: None,
20470 unions: Vec::new(),
20471 order_by: Vec::new(),
20472 limit: None,
20473 offset: None,
20474 limit_with_ties: false,
20475 window_check_exprs: Vec::new(),
20476 })
20477 }
20478
20479 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20480 /// variants) → a derived table that reads each declared column out of
20481 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20482 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20483 /// the scalar *record form projects a single row straight off `J`.
20484 /// Rides the existing lateral-subquery channel, so no new executor or
20485 /// AST is needed.
20486 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20487 use crate::ast::{
20488 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20489 };
20490 let fn_name = match self.peek() {
20491 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20492 _ => unreachable!("caller guarded is_json_to_record_name"),
20493 };
20494 self.advance(); // fn name
20495 self.advance(); // (
20496 let mut arg = self.parse_expr(0)?;
20497 // populate_record(base, json): the base only carries the record
20498 // type here — the JSON argument is the second expression.
20499 let mut base: Option<Expr> = None;
20500 if matches!(self.peek(), Token::Comma) {
20501 self.advance();
20502 base = Some(arg);
20503 arg = self.parse_expr(0)?;
20504 }
20505 if !matches!(self.peek(), Token::RParen) {
20506 return Err(self.err(alloc::format!(
20507 "expected ')' after {fn_name}() argument, got {:?}",
20508 self.peek()
20509 )));
20510 }
20511 self.advance(); // )
20512 let is_set = fn_name.ends_with("recordset");
20513 // `[AS] alias ( col type [, …] )` column-definition list.
20514 if matches!(self.peek(), Token::As) {
20515 self.advance();
20516 }
20517 let alias_opt = match self.peek() {
20518 Token::Ident(s) | Token::QuotedIdent(s) => {
20519 let a = s.clone();
20520 self.advance();
20521 Some(a)
20522 }
20523 _ => None,
20524 };
20525 // v7.39 (read01 round 76) — the populate family's canonical PG
20526 // spelling carries no column list at all: the row shape comes from
20527 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20528 // j)`). The parser has no catalog, so hand the two arguments to the
20529 // engine's table-function channel, which does. Only `*_to_record*`
20530 // (whose base is bare `record`) genuinely requires the list.
20531 if !matches!(self.peek(), Token::LParen) {
20532 if let Some(base_expr) = base {
20533 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20534 return Ok(TableRef {
20535 name: alias.clone(),
20536 alias: Some(alias),
20537 only: false,
20538 as_of_segment: None,
20539 unnest_expr: None,
20540 unnest_column_aliases: Vec::new(),
20541 with_ordinality: false,
20542 generate_series_args: None,
20543 lateral_subquery: None,
20544 jsonb_each_text_arg: None,
20545 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20546 rows_from: None,
20547 json_table: None,
20548 scalar_fn_item: false,
20549 });
20550 }
20551 return Err(self.err(alloc::format!(
20552 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20553 self.peek()
20554 )));
20555 }
20556 let Some(alias) = alias_opt else {
20557 return Err(self.err(alloc::format!(
20558 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20559 )));
20560 };
20561 self.advance(); // (
20562 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20563 loop {
20564 let col = self.expect_ident_like()?;
20565 let ty = self.parse_cast_target()?;
20566 coldefs.push((col, ty));
20567 if matches!(self.peek(), Token::Comma) {
20568 self.advance();
20569 continue;
20570 }
20571 if matches!(self.peek(), Token::RParen) {
20572 self.advance();
20573 break;
20574 }
20575 return Err(self.err(alloc::format!(
20576 "expected ',' or ')' in {fn_name} column list, got {:?}",
20577 self.peek()
20578 )));
20579 }
20580 if coldefs.is_empty() {
20581 return Err(self.err(alloc::format!(
20582 "{fn_name} column-definition list must declare at least one column"
20583 )));
20584 }
20585 // Per column: (base ->> 'col')::type AS col. The base is the
20586 // per-element `value` column for the *set form, or the argument
20587 // itself for the scalar record form.
20588 let items: Vec<SelectItem> = coldefs
20589 .into_iter()
20590 .map(|(col, ty)| {
20591 let base = if is_set {
20592 Expr::Column(ColumnName {
20593 qualifier: None,
20594 name: "value".to_string(),
20595 })
20596 } else {
20597 arg.clone()
20598 };
20599 SelectItem::Expr {
20600 expr: Expr::Cast {
20601 expr: Box::new(Expr::Binary {
20602 lhs: Box::new(base),
20603 op: BinOp::JsonGetText,
20604 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20605 }),
20606 target: ty,
20607 },
20608 alias: Some(col),
20609 }
20610 })
20611 .collect();
20612 let from = if is_set {
20613 let elem_fn = if fn_name.starts_with("jsonb") {
20614 "jsonb_array_elements"
20615 } else {
20616 "json_array_elements"
20617 };
20618 Some(FromClause {
20619 primary: TableRef {
20620 name: "value".to_string(),
20621 alias: None,
20622 only: false,
20623 as_of_segment: None,
20624 unnest_expr: Some(Box::new(Expr::FunctionCall {
20625 name: elem_fn.to_string(),
20626 args: alloc::vec![arg],
20627 })),
20628 unnest_column_aliases: alloc::vec!["value".to_string()],
20629 with_ordinality: false,
20630 generate_series_args: None,
20631 lateral_subquery: None,
20632 jsonb_each_text_arg: None,
20633 table_fn_call: None,
20634 rows_from: None,
20635 json_table: None,
20636 scalar_fn_item: false,
20637 },
20638 joins: Vec::new(),
20639 })
20640 } else {
20641 None
20642 };
20643 let inner = SelectStatement {
20644 locking: None,
20645 ctes: Vec::new(),
20646 distinct: false,
20647 distinct_on: Vec::new(),
20648 items,
20649 from,
20650 where_: None,
20651 group_by: None,
20652 group_by_all: false,
20653 having: None,
20654 unions: Vec::new(),
20655 order_by: Vec::new(),
20656 limit: None,
20657 offset: None,
20658 limit_with_ties: false,
20659 window_check_exprs: Vec::new(),
20660 };
20661 Ok(TableRef {
20662 name: alias.clone(),
20663 alias: Some(alias),
20664 only: false,
20665 as_of_segment: None,
20666 unnest_expr: None,
20667 unnest_column_aliases: Vec::new(),
20668 with_ordinality: false,
20669 generate_series_args: None,
20670 lateral_subquery: Some(Box::new(inner)),
20671 jsonb_each_text_arg: None,
20672 table_fn_call: None,
20673 rows_from: None,
20674 json_table: None,
20675 scalar_fn_item: false,
20676 })
20677 }
20678
20679 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20680 /// Returns true when the clause was present. `WITH` alone (a
20681 /// CTE can never start here) is not enough — the ORDINALITY
20682 /// ident must follow, so a stray WITH still errors downstream.
20683 fn absorb_with_ordinality(&mut self) -> bool {
20684 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20685 && matches!(self.tokens.get(self.pos + 1),
20686 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20687 {
20688 self.advance();
20689 self.advance();
20690 true
20691 } else {
20692 false
20693 }
20694 }
20695
20696 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20697 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20698 /// Out-of-line: the caller sits on the FROM recursion chain.
20699 #[inline(never)]
20700 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20701 let fn_name = match self.advance() {
20702 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20703 _ => unreachable!("caller peeked an ident"),
20704 };
20705 self.advance(); // (
20706 let mut args: Vec<Expr> = Vec::new();
20707 if !matches!(self.peek(), Token::RParen) {
20708 loop {
20709 args.push(self.parse_expr(0)?);
20710 if matches!(self.peek(), Token::Comma) {
20711 self.advance();
20712 continue;
20713 }
20714 break;
20715 }
20716 }
20717 if !matches!(self.peek(), Token::RParen) {
20718 return Err(self.err(alloc::format!(
20719 "expected ')' after {fn_name}() arguments, got {:?}",
20720 self.peek()
20721 )));
20722 }
20723 self.advance();
20724 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20725 // counter column rides after the function's own, and the alias list
20726 // names it.
20727 let with_ordinality = self.absorb_with_ordinality();
20728 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20729 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20730 Ok(TableRef {
20731 name,
20732 alias: alias_ident,
20733 only: false,
20734 as_of_segment: None,
20735 unnest_expr: None,
20736 unnest_column_aliases,
20737 with_ordinality,
20738 generate_series_args: None,
20739 lateral_subquery: None,
20740 jsonb_each_text_arg: None,
20741 table_fn_call: Some(Box::new((fn_name, args))),
20742 rows_from: None,
20743 json_table: None,
20744 scalar_fn_item: false,
20745 })
20746 }
20747
20748 /// v7.39 (round 205, JSON_TABLE) — parse
20749 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20750 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20751 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20752 #[inline(never)]
20753 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20754 self.advance(); // json_table
20755 self.advance(); // (
20756 let doc = Box::new(self.parse_expr(0)?);
20757 self.expect_comma_json_table()?;
20758 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20759 // Optional `PASSING <expr> AS <name> [, …]`.
20760 let mut passing: Vec<(String, Expr)> = Vec::new();
20761 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20762 self.advance();
20763 loop {
20764 let e = self.parse_expr(0)?;
20765 if !matches!(self.peek(), Token::As) {
20766 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20767 }
20768 self.advance();
20769 let vname = match self.advance() {
20770 Token::Ident(s) | Token::QuotedIdent(s) => s,
20771 other => {
20772 return Err(self.err(alloc::format!(
20773 "expected PASSING variable name, got {other:?}"
20774 )));
20775 }
20776 };
20777 passing.push((vname, e));
20778 if matches!(self.peek(), Token::Comma) {
20779 self.advance();
20780 continue;
20781 }
20782 break;
20783 }
20784 }
20785 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20786 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20787 }
20788 self.advance();
20789 let columns = self.parse_json_table_columns()?;
20790 if !matches!(self.peek(), Token::RParen) {
20791 return Err(self.err(alloc::format!(
20792 "expected ')' to close JSON_TABLE, got {:?}",
20793 self.peek()
20794 )));
20795 }
20796 self.advance();
20797 let alias_ident = self.parse_optional_alias()?;
20798 let name = alias_ident
20799 .clone()
20800 .unwrap_or_else(|| String::from("json_table"));
20801 Ok(TableRef {
20802 name,
20803 alias: alias_ident,
20804 only: false,
20805 as_of_segment: None,
20806 unnest_expr: None,
20807 unnest_column_aliases: Vec::new(),
20808 with_ordinality: false,
20809 generate_series_args: None,
20810 lateral_subquery: None,
20811 jsonb_each_text_arg: None,
20812 table_fn_call: None,
20813 rows_from: None,
20814 json_table: Some(Box::new(crate::ast::JsonTable {
20815 doc,
20816 row_path,
20817 columns,
20818 passing,
20819 })),
20820 scalar_fn_item: false,
20821 })
20822 }
20823
20824 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20825 if !matches!(self.peek(), Token::Comma) {
20826 return Err(self.err(alloc::format!(
20827 "expected ',' after JSON_TABLE document, got {:?}",
20828 self.peek()
20829 )));
20830 }
20831 self.advance();
20832 Ok(())
20833 }
20834
20835 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20836 match self.advance() {
20837 Token::String(s) => Ok(s),
20838 other => Err(self.err(alloc::format!(
20839 "expected {what} string literal, got {other:?}"
20840 ))),
20841 }
20842 }
20843
20844 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20845 #[inline(never)]
20846 fn parse_json_table_columns(
20847 &mut self,
20848 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20849 if !matches!(self.peek(), Token::LParen) {
20850 return Err(self.err("expected '(' after COLUMNS".into()));
20851 }
20852 self.advance();
20853 let mut cols = Vec::new();
20854 loop {
20855 cols.push(self.parse_json_table_one_column()?);
20856 if matches!(self.peek(), Token::Comma) {
20857 self.advance();
20858 continue;
20859 }
20860 break;
20861 }
20862 if !matches!(self.peek(), Token::RParen) {
20863 return Err(self.err(alloc::format!(
20864 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20865 self.peek()
20866 )));
20867 }
20868 self.advance();
20869 Ok(cols)
20870 }
20871
20872 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20873 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20874 // NESTED [PATH] '<p>' COLUMNS (...)
20875 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20876 self.advance();
20877 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20878 self.advance();
20879 }
20880 let path = self.parse_json_string_literal("NESTED PATH")?;
20881 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20882 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20883 }
20884 self.advance();
20885 let columns = self.parse_json_table_columns()?;
20886 return Ok(JsonTableColumn::Nested { path, columns });
20887 }
20888 // <name> ...
20889 let name = match self.advance() {
20890 Token::Ident(s) | Token::QuotedIdent(s) => s,
20891 other => {
20892 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20893 }
20894 };
20895 // <name> FOR ORDINALITY
20896 if matches!(self.peek(), Token::For) {
20897 self.advance();
20898 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20899 return Err(self.err("expected ORDINALITY after FOR".into()));
20900 }
20901 self.advance();
20902 return Ok(JsonTableColumn::Ordinality { name });
20903 }
20904 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20905 let ty = self.parse_column_type_name()?;
20906 let mut format_json = false;
20907 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20908 self.advance();
20909 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20910 return Err(self.err("expected JSON after FORMAT".into()));
20911 }
20912 self.advance();
20913 format_json = true;
20914 }
20915 let mut exists = false;
20916 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20917 self.advance();
20918 exists = true;
20919 }
20920 let mut path = alloc::format!("$.{name}");
20921 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20922 self.advance();
20923 path = self.parse_json_string_literal("column PATH")?;
20924 }
20925 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20926 // `FORMAT JSON` after PATH (alternate placement).
20927 self.advance();
20928 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20929 self.advance();
20930 }
20931 format_json = true;
20932 }
20933 let mut wrapper = false;
20934 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20935 self.advance();
20936 // optional CONDITIONAL/UNCONDITIONAL
20937 if matches!(self.peek(), Token::Ident(s)
20938 if s.eq_ignore_ascii_case("unconditional")
20939 || s.eq_ignore_ascii_case("conditional"))
20940 {
20941 self.advance();
20942 }
20943 if !matches!(self.peek(), Token::Ident(s)
20944 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20945 {
20946 return Err(self.err("expected WRAPPER after WITH".into()));
20947 }
20948 self.advance();
20949 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20950 if matches!(self.peek(), Token::Ident(s)
20951 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20952 {
20953 self.advance();
20954 }
20955 wrapper = true;
20956 }
20957 // ON EMPTY / ON ERROR clauses (two, in any order).
20958 let mut on_empty = JsonTableOnBehavior::Null;
20959 let mut on_error = JsonTableOnBehavior::Null;
20960 for _ in 0..2 {
20961 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20962 {
20963 self.advance();
20964 Some(JsonTableOnBehavior::Error)
20965 } else if matches!(self.peek(), Token::Null) {
20966 self.advance();
20967 Some(JsonTableOnBehavior::Null)
20968 } else if matches!(self.peek(), Token::Default) {
20969 self.advance();
20970 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20971 } else {
20972 None
20973 };
20974 let Some(behavior) = behavior else { break };
20975 // `ON {EMPTY|ERROR}`
20976 if !matches!(self.peek(), Token::On) {
20977 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20978 }
20979 self.advance();
20980 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20981 self.advance();
20982 on_empty = behavior;
20983 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20984 self.advance();
20985 on_error = behavior;
20986 } else {
20987 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20988 }
20989 }
20990 Ok(JsonTableColumn::Regular {
20991 name,
20992 ty,
20993 path,
20994 exists,
20995 format_json,
20996 wrapper,
20997 on_empty,
20998 on_error,
20999 })
21000 }
21001
21002 fn parse_optional_alias_with_columns(
21003 &mut self,
21004 ) -> Result<(Option<String>, Vec<String>), ParseError> {
21005 let alias = self.parse_optional_alias()?;
21006 if alias.is_none() {
21007 return Ok((None, Vec::new()));
21008 }
21009 let mut cols: Vec<String> = Vec::new();
21010 if matches!(self.peek(), Token::LParen) {
21011 self.advance();
21012 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21013 self.advance();
21014 cols.push(s);
21015 if matches!(self.peek(), Token::Comma) {
21016 self.advance();
21017 continue;
21018 }
21019 break;
21020 }
21021 if matches!(self.peek(), Token::RParen) {
21022 self.advance();
21023 }
21024 }
21025 Ok((alias, cols))
21026 }
21027
21028 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21029 /// whose keyword token was already consumed and whose `(` is the
21030 /// current token. Factored out of `parse_atom` (and marked
21031 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21032 /// recursive `parse_atom` frame — inlining them there enlarges the
21033 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21034 /// against, risking an overflow before the budget triggers.
21035 #[inline(never)]
21036 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21037 self.advance(); // (
21038 let mut args = Vec::new();
21039 if !matches!(self.peek(), Token::RParen) {
21040 loop {
21041 args.push(self.parse_expr(0)?);
21042 match self.peek() {
21043 Token::Comma => {
21044 self.advance();
21045 }
21046 Token::RParen => break,
21047 other => {
21048 return Err(self.err(alloc::format!(
21049 "expected ',' or ')' in {name}() args, got {other:?}"
21050 )));
21051 }
21052 }
21053 }
21054 }
21055 self.advance(); // )
21056 Ok(Expr::FunctionCall {
21057 name: name.into(),
21058 args,
21059 })
21060 }
21061
21062 /// FROM-clause: a primary table reference plus zero-or-more joined
21063 /// peers expressed via either `, <table>` (cross-product, no ON) or
21064 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21065 /// v1.10 keeps the join list flat (left-associative nested-loop
21066 /// semantics).
21067 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21068 let primary = self.parse_table_ref()?;
21069 let primary_qual = primary
21070 .alias
21071 .clone()
21072 .unwrap_or_else(|| primary.name.clone());
21073 let joins = self.parse_from_joins(&primary_qual)?;
21074 Ok(FromClause { primary, joins })
21075 }
21076
21077 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21078 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21079 /// SAME grammar after its target table has already been consumed.
21080 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21081 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21082 /// be parsed forward, once.)
21083 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21084 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21085 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21086 /// desugaring, which needs a name for the left side of each equality.
21087 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21088 let mut joins = Vec::new();
21089 loop {
21090 // `, <table>` — cross-product with no ON.
21091 if matches!(self.peek(), Token::Comma) {
21092 self.advance();
21093 let table = self.parse_table_ref()?;
21094 joins.push(FromJoin {
21095 kind: JoinKind::Cross,
21096 table,
21097 on: None,
21098 using_cols: None,
21099 natural: false,
21100 });
21101 continue;
21102 }
21103 // v7.37.16 — optional leading `NATURAL` before the join
21104 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21105 // not a lexer keyword (it arrives as a bare Ident), so match
21106 // it case-insensitively here. When present, no ON/USING
21107 // clause is allowed — the common columns are resolved at
21108 // execution time.
21109 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21110 if natural {
21111 self.advance();
21112 }
21113 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21114 // CROSS JOIN, and bare JOIN (defaults to INNER).
21115 let kind =
21116 match self.peek() {
21117 Token::Inner => {
21118 self.advance();
21119 if !matches!(self.peek(), Token::Join) {
21120 return Err(self
21121 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21122 }
21123 self.advance();
21124 JoinKind::Inner
21125 }
21126 Token::Left => {
21127 self.advance();
21128 if matches!(self.peek(), Token::Outer) {
21129 self.advance();
21130 }
21131 if !matches!(self.peek(), Token::Join) {
21132 return Err(self.err(format!(
21133 "expected JOIN after LEFT [OUTER], got {:?}",
21134 self.peek()
21135 )));
21136 }
21137 self.advance();
21138 JoinKind::Left
21139 }
21140 Token::Cross => {
21141 self.advance();
21142 if !matches!(self.peek(), Token::Join) {
21143 return Err(self
21144 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21145 }
21146 self.advance();
21147 JoinKind::Cross
21148 }
21149 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21150 Token::Right => {
21151 self.advance();
21152 if matches!(self.peek(), Token::Outer) {
21153 self.advance();
21154 }
21155 if !matches!(self.peek(), Token::Join) {
21156 return Err(self.err(format!(
21157 "expected JOIN after RIGHT [OUTER], got {:?}",
21158 self.peek()
21159 )));
21160 }
21161 self.advance();
21162 JoinKind::Right
21163 }
21164 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21165 Token::Full => {
21166 self.advance();
21167 if matches!(self.peek(), Token::Outer) {
21168 self.advance();
21169 }
21170 if !matches!(self.peek(), Token::Join) {
21171 return Err(self.err(format!(
21172 "expected JOIN after FULL [OUTER], got {:?}",
21173 self.peek()
21174 )));
21175 }
21176 self.advance();
21177 JoinKind::FullOuter
21178 }
21179 Token::Join => {
21180 self.advance();
21181 JoinKind::Inner
21182 }
21183 _ => break,
21184 };
21185 let table = self.parse_table_ref()?;
21186 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21187 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21188 // where prev_table is the most-recent left-side table
21189 // (the previous join's table if any, else the FROM primary).
21190 // PG semantics around column merging are richer (USING'd
21191 // cols become deduplicated single output columns); for
21192 // sugar purposes the predicate-only form covers the
21193 // baseline corpus shape and chained `… JOIN x USING (k)
21194 // JOIN y USING (k)` calls.
21195 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21196 // common columns resolve at execution time.
21197 if natural {
21198 joins.push(FromJoin {
21199 kind,
21200 table,
21201 on: None,
21202 using_cols: None,
21203 natural: true,
21204 });
21205 continue;
21206 }
21207 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21208 // v7.37.16 — capture the USING column list (in addition to
21209 // the ON desugar below) so the executor can perform PG's
21210 // column-merge on the output side.
21211 let mut using_cols: Option<Vec<String>> = None;
21212 let on = if matches!(self.peek(), Token::On) {
21213 self.advance();
21214 Some(self.parse_expr(0)?)
21215 } else if using_match {
21216 self.advance();
21217 if !matches!(self.peek(), Token::LParen) {
21218 return Err(
21219 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21220 );
21221 }
21222 self.advance();
21223 let mut cols: Vec<String> = Vec::new();
21224 loop {
21225 match self.peek().clone() {
21226 Token::Ident(s) | Token::QuotedIdent(s) => {
21227 self.advance();
21228 cols.push(s);
21229 }
21230 other => {
21231 return Err(self.err(format!(
21232 "expected column name inside USING (…), got {other:?}"
21233 )));
21234 }
21235 }
21236 match self.peek() {
21237 Token::Comma => {
21238 self.advance();
21239 continue;
21240 }
21241 Token::RParen => {
21242 self.advance();
21243 break;
21244 }
21245 other => {
21246 return Err(self.err(format!(
21247 "expected ',' or ')' inside USING (…), got {other:?}"
21248 )));
21249 }
21250 }
21251 }
21252 if cols.is_empty() {
21253 return Err(self.err("USING (…) requires at least one column".to_string()));
21254 }
21255 using_cols = Some(cols.clone());
21256 // Pick the left-side alias: prev join's table if any,
21257 // else FROM primary. Use alias when present, else
21258 // table name (PG-equivalent qualifier).
21259 let left_qual: String = joins
21260 .last()
21261 .map(|j| {
21262 j.table
21263 .alias
21264 .clone()
21265 .unwrap_or_else(|| j.table.name.clone())
21266 })
21267 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21268 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21269 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21270 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21271 qualifier: Some(left_qual.clone()),
21272 name: c.clone(),
21273 })),
21274 op: crate::ast::BinOp::Eq,
21275 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21276 qualifier: Some(right_qual.clone()),
21277 name: c,
21278 })),
21279 });
21280 let first = iter.next().expect("at least one col");
21281 Some(iter.fold(first, |acc, pred| Expr::Binary {
21282 lhs: alloc::boxed::Box::new(acc),
21283 op: crate::ast::BinOp::And,
21284 rhs: alloc::boxed::Box::new(pred),
21285 }))
21286 } else if kind == JoinKind::Cross {
21287 None
21288 } else {
21289 return Err(self.err(format!(
21290 "expected ON or USING after {:?} JOIN, got {:?}",
21291 kind,
21292 self.peek()
21293 )));
21294 };
21295 joins.push(FromJoin {
21296 kind,
21297 table,
21298 on,
21299 using_cols,
21300 natural: false,
21301 });
21302 }
21303 Ok(joins)
21304 }
21305
21306 /// Optional alias after an expression or table:
21307 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21308 /// accepted (PG-style implicit alias). Returns `None` if the next token
21309 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21310 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21311 if matches!(self.peek(), Token::As) {
21312 self.advance();
21313 // v7.39 (round 340, V56) — after AS the next token MUST be an
21314 // identifier. This used to return None and "let the caller
21315 // surface the error on the next expectation", but when AS is
21316 // the LAST token there is no next expectation: `SELECT 1 AS`
21317 // parsed clean and silently dropped the alias. PG rejects it.
21318 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21319 return self.expect_ident_like().map(Some);
21320 }
21321 return Err(self.err(alloc::format!(
21322 "expected an alias after AS, got {:?}",
21323 self.peek()
21324 )));
21325 }
21326 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21327 // grammar reserves a long list of follow-keywords from the
21328 // alias slot. SPG's bareword approximation: skip a small
21329 // set of idents that would otherwise be swallowed as the
21330 // table alias and break trailing clauses like CREATE
21331 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21332 // CONFLICT WHERE shapes.
21333 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21334 if is_alias_stopword(s) {
21335 return Ok(None);
21336 }
21337 return Ok(self.expect_ident_like().ok());
21338 }
21339 Ok(None)
21340 }
21341
21342 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21343 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21344 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21345 // error beats a stack overflow (an overflow aborts the
21346 // embedding host process).
21347 self.enter_nested()?;
21348 let r = self.parse_expr_inner(min_prec);
21349 self.nest_depth -= 1;
21350 r
21351 }
21352
21353 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21354 /// When the upcoming tokens form one, return the underlying
21355 /// operator token and the position just past the closing paren
21356 /// so the binary loop can dispatch on the plain operator.
21357 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21358 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21359 return None;
21360 }
21361 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21362 return None;
21363 }
21364 let mut i = self.pos + 2;
21365 // Optional schema qualifier (pg_catalog.<op> etc.).
21366 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21367 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21368 {
21369 i += 2;
21370 }
21371 let op_tok = self.tokens.get(i)?.clone();
21372 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21373 return None;
21374 }
21375 Some((i + 2, op_tok))
21376 }
21377
21378 /// PG operator symbols that lower onto function calls in
21379 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21380 /// family → regexp_like, comparison rung), `^@` (starts_with,
21381 /// comparison rung), `^` (power, tighter than `*`), `#`
21382 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21383 /// subset of the OR bits so the subtraction never borrows).
21384 fn try_symbol_operator(
21385 &mut self,
21386 lhs: &Expr,
21387 min_prec: u8,
21388 ) -> Result<Option<Expr>, ParseError> {
21389 enum Sym {
21390 Regex { ci: bool, negated: bool },
21391 Like { ci: bool, negated: bool },
21392 StartsWith,
21393 Power,
21394 Xor,
21395 RangeAdjacent,
21396 }
21397 // v7.39 (IS-precedence knife) — the low-precedence postfix
21398 // predicates ride this existing leaf call (zero new frame slots
21399 // on the nesting chain).
21400 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21401 return Ok(Some(e));
21402 }
21403 let (sym, prec): (Sym, u8) = match self.peek() {
21404 Token::Tilde => (
21405 Sym::Regex {
21406 ci: false,
21407 negated: false,
21408 },
21409 5,
21410 ),
21411 Token::TildeStar => (
21412 Sym::Regex {
21413 ci: true,
21414 negated: false,
21415 },
21416 5,
21417 ),
21418 Token::NotTilde => (
21419 Sym::Regex {
21420 ci: false,
21421 negated: true,
21422 },
21423 5,
21424 ),
21425 Token::NotTildeStar => (
21426 Sym::Regex {
21427 ci: true,
21428 negated: true,
21429 },
21430 5,
21431 ),
21432 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21433 Token::DoubleTilde => (
21434 Sym::Like {
21435 ci: false,
21436 negated: false,
21437 },
21438 5,
21439 ),
21440 Token::DoubleTildeStar => (
21441 Sym::Like {
21442 ci: true,
21443 negated: false,
21444 },
21445 5,
21446 ),
21447 Token::NotDoubleTilde => (
21448 Sym::Like {
21449 ci: false,
21450 negated: true,
21451 },
21452 5,
21453 ),
21454 Token::NotDoubleTildeStar => (
21455 Sym::Like {
21456 ci: true,
21457 negated: true,
21458 },
21459 5,
21460 ),
21461 Token::CaretAt => (Sym::StartsWith, 5),
21462 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21463 // tighter than `* / & |`, which the prec-9 rung preserves —
21464 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21465 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21466 Token::Caret => (Sym::Power, 9),
21467 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21468 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21469 Token::Hash => (Sym::Xor, 6),
21470 Token::Adjacent => (Sym::RangeAdjacent, 5),
21471 _ => return Ok(None),
21472 };
21473 if prec < min_prec {
21474 return Ok(None);
21475 }
21476 self.advance();
21477 let rhs = self.parse_expr(prec + 1)?;
21478 let out = match sym {
21479 Sym::Regex { ci, negated } => {
21480 let mut args = alloc::vec![lhs.clone(), rhs];
21481 if ci {
21482 args.push(Expr::Literal(Literal::String(String::from("i"))));
21483 }
21484 maybe_not(
21485 Expr::FunctionCall {
21486 name: String::from("regexp_like"),
21487 args,
21488 },
21489 negated,
21490 )
21491 }
21492 Sym::Like { ci, negated } => Expr::Like {
21493 expr: alloc::boxed::Box::new(lhs.clone()),
21494 pattern: alloc::boxed::Box::new(rhs),
21495 negated,
21496 case_insensitive: ci,
21497 },
21498 Sym::StartsWith => Expr::FunctionCall {
21499 name: String::from("starts_with"),
21500 args: alloc::vec![lhs.clone(), rhs],
21501 },
21502 Sym::Power => Expr::FunctionCall {
21503 name: String::from("power"),
21504 args: alloc::vec![lhs.clone(), rhs],
21505 },
21506 // `#` bitwise XOR — a real operator now (was desugared to
21507 // `(a|b)-(a&b)`, algebraically identical for integers but
21508 // undefined for bit strings; the direct op handles both).
21509 Sym::Xor => Expr::Binary {
21510 lhs: Box::new(lhs.clone()),
21511 op: BinOp::BitXor,
21512 rhs: Box::new(rhs),
21513 },
21514 // range `-|-` "is adjacent to" — lowered to a catalog function.
21515 Sym::RangeAdjacent => Expr::FunctionCall {
21516 name: String::from("range_adjacent"),
21517 args: alloc::vec![lhs.clone(), rhs],
21518 },
21519 };
21520 Ok(Some(out))
21521 }
21522
21523 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21524 /// predicates, moved out of the tight postfix-cast loop: PG binds
21525 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21526 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21527 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21528 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21529 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21530 /// when nothing at this position belongs to the family. Out-of-line
21531 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21532 /// chain that MAX_NEST_DEPTH is tuned against.
21533 #[inline(never)]
21534 fn parse_postfix_predicate(
21535 &mut self,
21536 lhs: &Expr,
21537 min_prec: u8,
21538 ) -> Result<Option<Expr>, ParseError> {
21539 // Reached through try_symbol_operator (an existing leaf call of
21540 // the binary loop) so NO new stack slots land on the per-nesting
21541 // frame chain; the lhs clones only when a predicate actually
21542 // consumes it.
21543 match self.peek() {
21544 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21545 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21546 Token::Is if min_prec <= 4 => {}
21547 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21548 Token::Not
21549 if min_prec <= 5
21550 && matches!(
21551 self.tokens.get(self.pos + 1),
21552 Some(Token::Between | Token::In | Token::Like)
21553 ) => {}
21554 Token::Not | Token::Ident(_)
21555 if min_prec <= 5
21556 && (matches!(self.peek(), Token::Ident(s)
21557 if s.eq_ignore_ascii_case("ilike")
21558 || (self.mysql_dialect
21559 && (s.eq_ignore_ascii_case("regexp")
21560 || s.eq_ignore_ascii_case("rlike")))
21561 || (s.eq_ignore_ascii_case("similar")
21562 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21563 || (matches!(self.peek(), Token::Not)
21564 && matches!(self.tokens.get(self.pos + 1),
21565 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21566 || (self.mysql_dialect
21567 && (s.eq_ignore_ascii_case("regexp")
21568 || s.eq_ignore_ascii_case("rlike")))
21569 || s.eq_ignore_ascii_case("similar")))) => {}
21570 _ => return Ok(None),
21571 }
21572 let mut expr = lhs.clone();
21573 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21574 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21575 if min_prec <= 4 {
21576 if matches!(self.peek(), Token::Is) {
21577 self.advance();
21578 let negated = if matches!(self.peek(), Token::Not) {
21579 self.advance();
21580 true
21581 } else {
21582 false
21583 };
21584 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21585 // mailrs pg_dump.
21586 if matches!(self.peek(), Token::Distinct) {
21587 self.advance();
21588 if !matches!(self.peek(), Token::From) {
21589 return Err(self.err(format!(
21590 "expected FROM after IS{} DISTINCT, got {:?}",
21591 if negated { " NOT" } else { "" },
21592 self.peek()
21593 )));
21594 }
21595 self.advance();
21596 // Right-hand side: parse at the same precedence
21597 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21598 // groups as `x IS DISTINCT FROM (a + b)`.
21599 let rhs = self.parse_expr(5)?;
21600 let op = if negated {
21601 BinOp::IsNotDistinctFrom
21602 } else {
21603 BinOp::IsDistinctFrom
21604 };
21605 expr = Expr::Binary {
21606 op,
21607 lhs: Box::new(expr),
21608 rhs: Box::new(rhs),
21609 };
21610 {
21611 return Ok(Some(expr));
21612 }
21613 }
21614 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21615 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21616 // Lowers onto pg_is_json(x, kind); NOT wraps the
21617 // call in a logical negation.
21618 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21619 if s.eq_ignore_ascii_case("json"))
21620 {
21621 self.advance(); // JSON
21622 let kind = match self.peek() {
21623 Token::Ident(s) | Token::QuotedIdent(s)
21624 if matches!(
21625 s.to_ascii_lowercase().as_str(),
21626 "value" | "object" | "array" | "scalar"
21627 ) =>
21628 {
21629 let k = s.to_ascii_lowercase();
21630 self.advance();
21631 k
21632 }
21633 _ => "value".to_string(),
21634 };
21635 let call = Expr::FunctionCall {
21636 name: "pg_is_json".to_string(),
21637 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21638 };
21639 expr = if negated {
21640 Expr::Unary {
21641 op: UnOp::Not,
21642 expr: Box::new(call),
21643 }
21644 } else {
21645 call
21646 };
21647 {
21648 return Ok(Some(expr));
21649 }
21650 }
21651 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21652 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21653 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21654 {
21655 let form_kw = match self.peek() {
21656 Token::Ident(s) | Token::QuotedIdent(s)
21657 if matches!(
21658 s.to_ascii_uppercase().as_str(),
21659 "NFC" | "NFD" | "NFKC" | "NFKD"
21660 ) && matches!(
21661 self.tokens.get(self.pos + 1),
21662 Some(Token::Ident(n) | Token::QuotedIdent(n))
21663 if n.eq_ignore_ascii_case("normalized")
21664 ) =>
21665 {
21666 Some(s.to_ascii_uppercase())
21667 }
21668 _ => None,
21669 };
21670 let bare_normalized = form_kw.is_none()
21671 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21672 if s.eq_ignore_ascii_case("normalized"));
21673 if form_kw.is_some() || bare_normalized {
21674 if form_kw.is_some() {
21675 self.advance(); // form keyword
21676 }
21677 self.advance(); // NORMALIZED
21678 let mut args = alloc::vec![expr];
21679 if let Some(f) = form_kw {
21680 args.push(Expr::Literal(Literal::String(f)));
21681 }
21682 let call = Expr::FunctionCall {
21683 name: "is_normalized".to_string(),
21684 args,
21685 };
21686 expr = if negated {
21687 Expr::Unary {
21688 op: UnOp::Not,
21689 expr: Box::new(call),
21690 }
21691 } else {
21692 call
21693 };
21694 {
21695 return Ok(Some(expr));
21696 }
21697 }
21698 }
21699 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21700 // three-valued boolean tests. IS TRUE/FALSE never
21701 // return NULL, so they lower to CASE forms whose
21702 // ELSE catches the NULL branch; IS UNKNOWN on a
21703 // boolean is exactly IS NULL.
21704 if matches!(self.peek(), Token::True | Token::False)
21705 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21706 {
21707 let tok = self.advance();
21708 let test = match tok {
21709 Token::True => Some(true),
21710 Token::False => Some(false),
21711 _ => None, // UNKNOWN
21712 };
21713 // v7.39 (round 328, V45) — kept as what the user
21714 // wrote. These used to be lowered here into `CASE` /
21715 // `IS NULL`; the semantics were right but the AST no
21716 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21717 // was echoed back as
21718 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21719 expr = Expr::BoolTest {
21720 expr: Box::new(expr),
21721 value: test,
21722 negated,
21723 };
21724 {
21725 return Ok(Some(expr));
21726 }
21727 }
21728 if !matches!(self.peek(), Token::Null) {
21729 return Err(self.err(format!(
21730 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21731 if negated { " NOT" } else { "" },
21732 self.peek()
21733 )));
21734 }
21735 self.advance();
21736 expr = Expr::IsNull {
21737 expr: Box::new(expr),
21738 negated,
21739 };
21740 {
21741 return Ok(Some(expr));
21742 }
21743 }
21744 }
21745 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21746 if min_prec <= 5 {
21747 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21748 // Look one token ahead so a stray `NOT` not followed by any of
21749 // these flows through to the early return below untouched.
21750 let negated = if matches!(self.peek(), Token::Not) {
21751 let next = self.tokens.get(self.pos + 1);
21752 matches!(next, Some(Token::Between | Token::In | Token::Like))
21753 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21754 || (self.mysql_dialect
21755 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21756 || s.eq_ignore_ascii_case("similar"))
21757 } else {
21758 false
21759 };
21760 if negated {
21761 self.advance();
21762 }
21763 if matches!(self.peek(), Token::Between) {
21764 expr = self.parse_between_tail(expr, negated)?;
21765 {
21766 return Ok(Some(expr));
21767 }
21768 }
21769 if matches!(self.peek(), Token::In) {
21770 if self.suppress_in_tail && !negated {
21771 // POSITION(sub IN str) — IN belongs to the
21772 // enclosing function syntax; stop here.
21773 {
21774 return Ok(None);
21775 }
21776 }
21777 expr = self.parse_in_tail(expr, negated)?;
21778 {
21779 return Ok(Some(expr));
21780 }
21781 }
21782 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21783 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21784 // (the SQL→regex transform runs inside, in the backtracking-
21785 // friendly shape SPG's matcher needs).
21786 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21787 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21788 {
21789 self.advance(); // SIMILAR
21790 self.advance(); // TO
21791 let pattern = self.parse_expr(6)?;
21792 let mut args = alloc::vec![expr, pattern];
21793 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21794 self.advance();
21795 args.push(self.parse_expr(6)?);
21796 }
21797 let call = Expr::FunctionCall {
21798 name: "__similar_to".to_string(),
21799 args,
21800 };
21801 expr = maybe_not(call, negated);
21802 {
21803 return Ok(Some(expr));
21804 }
21805 }
21806 if matches!(self.peek(), Token::Like) {
21807 self.advance();
21808 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21809 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21810 expr = q;
21811 {
21812 return Ok(Some(expr));
21813 }
21814 }
21815 // Pattern at the same precedence as other comparison RHSes —
21816 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21817 let mut pattern = self.parse_expr(6)?;
21818 // `ESCAPE 'c'` — rewrite a literal pattern to the
21819 // default backslash escape at parse time. Custom
21820 // escapes on non-literal patterns would need
21821 // matcher support; error honestly.
21822 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21823 self.advance();
21824 let esc = self.parse_expr(6)?;
21825 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21826 }
21827 expr = Expr::Like {
21828 expr: Box::new(expr),
21829 pattern: Box::new(pattern),
21830 negated,
21831 case_insensitive: false,
21832 };
21833 {
21834 return Ok(Some(expr));
21835 }
21836 }
21837 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21838 // keyword reaches us as a plain identifier.
21839 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21840 self.advance();
21841 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21842 expr = q;
21843 {
21844 return Ok(Some(expr));
21845 }
21846 }
21847 let pattern = self.parse_expr(6)?;
21848 expr = Expr::Like {
21849 expr: Box::new(expr),
21850 pattern: Box::new(pattern),
21851 negated,
21852 case_insensitive: true,
21853 };
21854 {
21855 return Ok(Some(expr));
21856 }
21857 }
21858 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21859 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21860 // matches case-insensitively under the default collation, so it
21861 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21862 // `~*` operator uses, wrapped in NOT when negated.
21863 if self.mysql_dialect
21864 && matches!(self.peek(), Token::Ident(s)
21865 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21866 {
21867 self.advance();
21868 let pattern = self.parse_expr(6)?;
21869 let call = Expr::FunctionCall {
21870 name: String::from("regexp_like"),
21871 args: alloc::vec![
21872 expr,
21873 pattern,
21874 Expr::Literal(Literal::String(String::from("i"))),
21875 ],
21876 };
21877 return Ok(Some(maybe_not(call, negated)));
21878 }
21879 }
21880 let _ = expr;
21881 Ok(None)
21882 }
21883
21884 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21885 let mut lhs = self.parse_unary()?;
21886 let mut chain_len = 0usize;
21887 loop {
21888 // OPERATOR([schema.]op) reduces to its underlying
21889 // operator token before the normal dispatch.
21890 let explicit = self.peek_explicit_operator();
21891 let dispatch = match &explicit {
21892 Some((_, tok)) => self.binop_here(tok),
21893 None => self.binop_here(self.peek()),
21894 };
21895 let Some((op, prec)) = dispatch else {
21896 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21897 // of the symbol family. `binop_here` answers None for them
21898 // because they lower onto function calls rather than a
21899 // BinOp, and the fallback below reads `self.peek()` — the
21900 // word OPERATOR, not the operator. `pg_dump` writes every
21901 // catalog predicate this way, so its first query failed
21902 // and no dump ran:
21903 //
21904 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21905 //
21906 // Collapsing the wrapper to the operator it names puts the
21907 // token where the fallback already looks.
21908 if let Some((next, op_tok)) = explicit {
21909 self.tokens.splice(self.pos..next, [op_tok]);
21910 }
21911 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21912 lhs = e;
21913 chain_len += 1;
21914 if chain_len > MAX_BINARY_CHAIN {
21915 return Err(self.err(alloc::format!(
21916 "more than {MAX_BINARY_CHAIN} chained binary operators"
21917 )));
21918 }
21919 continue;
21920 }
21921 break;
21922 };
21923 if prec < min_prec {
21924 break;
21925 }
21926 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21927 // iteratively but evaluates and drops recursively;
21928 // depth beyond the budget overflows worker stacks.
21929 chain_len += 1;
21930 if chain_len > MAX_BINARY_CHAIN {
21931 return Err(self.err(alloc::format!(
21932 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21933 )));
21934 }
21935 match explicit {
21936 Some((end_pos, _)) => self.pos = end_pos,
21937 None => {
21938 self.advance();
21939 }
21940 }
21941 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21942 // ANY is a bare ident; ALL is a reserved Token. Both
21943 // require an immediate `(` to disambiguate from
21944 // identifier columns named `any` / `all`.
21945 let any_kind = match self.peek() {
21946 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21947 Some(false)
21948 }
21949 Token::Ident(s) | Token::QuotedIdent(s)
21950 if (s.eq_ignore_ascii_case("any")
21951 || s.eq_ignore_ascii_case("some")
21952 || s.eq_ignore_ascii_case("all"))
21953 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21954 {
21955 Some(!s.eq_ignore_ascii_case("all"))
21956 }
21957 _ => None,
21958 };
21959 if let Some(is_any) = any_kind {
21960 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21961 continue;
21962 }
21963 let rhs = self.parse_expr(prec + 1)?;
21964 lhs = Expr::Binary {
21965 lhs: Box::new(lhs),
21966 op,
21967 rhs: Box::new(rhs),
21968 };
21969 }
21970 Ok(lhs)
21971 }
21972
21973 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21974 /// and the array form.
21975 ///
21976 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21977 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21978 /// this block's `Expr` temporaries and four `format!` sites slots in
21979 /// that frame on every level of `((((1))))`, which never reaches it.
21980 #[inline(never)]
21981 fn parse_any_all_rhs(
21982 &mut self,
21983 lhs: Expr,
21984 op: BinOp,
21985 is_any: bool,
21986 ) -> Result<Expr, ParseError> {
21987 self.advance(); // ident
21988 self.advance(); // (
21989 // `x op ANY (SELECT …)` — the quantified-subquery
21990 // form. `= ANY` is exactly IN; the other operators
21991 // lower onto EXISTS over the subquery as a derived
21992 // table, comparing against its single projection
21993 // aliased __v (x's columns resolve correlated).
21994 // ALL is the negated-EXISTS complement; a NULL
21995 // element makes PG return NULL where this lowering
21996 // returns true — the NOT NULL column case (the
21997 // practical one) is exact.
21998 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21999 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22000 // legal PG too (round-151 sibling). Out-of-line
22001 // (#[inline(never)] helper) — this sits on
22002 // parse_expr's recursive frame and the two-armed
22003 // SELECT temporary blew the nesting-budget stack.
22004 let mut sub = self.parse_any_all_select_body()?;
22005 if !matches!(self.peek(), Token::RParen) {
22006 return Err(self.err(alloc::format!(
22007 "expected ')' after ANY/ALL subquery, got {:?}",
22008 self.peek()
22009 )));
22010 }
22011 self.advance();
22012 if sub.items.len() != 1 {
22013 return Err(self.err(alloc::format!(
22014 "ANY/ALL subquery must return one column, got {}",
22015 sub.items.len()
22016 )));
22017 }
22018 if is_any && matches!(op, BinOp::Eq) {
22019 return Ok(Expr::InSubquery {
22020 expr: Box::new(lhs),
22021 subquery: Box::new(sub),
22022 negated: false,
22023 });
22024 }
22025 // The engine's subquery resolvers materialise
22026 // the single-column result into an ARRAY the
22027 // existing AnyAll three-valued eval consumes.
22028 return Ok(Expr::AnyAll {
22029 expr: Box::new(lhs),
22030 op,
22031 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22032 is_any,
22033 });
22034 }
22035 let arr = self.parse_expr(0)?;
22036 if !matches!(self.peek(), Token::RParen) {
22037 return Err(self.err(alloc::format!(
22038 "expected ')' after ANY/ALL argument, got {:?}",
22039 self.peek()
22040 )));
22041 }
22042 self.advance();
22043 Ok(Expr::AnyAll {
22044 expr: Box::new(lhs),
22045 op,
22046 array: Box::new(arr),
22047 is_any,
22048 })
22049 }
22050
22051 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22052 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22053 #[inline(never)]
22054 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22055 self.advance();
22056 let e = self.parse_expr(9)?;
22057 Ok(build_center_call(e))
22058 }
22059
22060 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22061 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22062 /// unary minus.
22063 ///
22064 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22065 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22066 /// the Expr-sized local stays out of that frame.
22067 #[inline(never)]
22068 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22069 self.advance();
22070 let e = self.parse_expr(9)?;
22071 Ok(Expr::FunctionCall {
22072 name: alloc::string::String::from(name),
22073 args: alloc::vec![e],
22074 })
22075 }
22076
22077 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22078 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22079 #[inline(never)]
22080 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22081 self.advance();
22082 let e = self.parse_expr(9)?;
22083 Ok(Expr::FunctionCall {
22084 name: alloc::string::String::from(if vertical {
22085 "isvertical"
22086 } else {
22087 "ishorizontal"
22088 }),
22089 args: alloc::vec![e],
22090 })
22091 }
22092
22093 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22094 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22095 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22096 #[inline(never)]
22097 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22098 self.advance();
22099 let e = self.parse_expr(9)?;
22100 Ok(Expr::Cast {
22101 expr: Box::new(e),
22102 target: CastTarget::Named("binary".to_string()),
22103 })
22104 }
22105
22106 /// The prefix operators that share one shape: take the token, parse
22107 /// an operand at `prec`, wrap it.
22108 ///
22109 /// `#[inline(never)]`, and one function instead of five arms, for the
22110 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22111 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22112 /// debug build gives EVERY arm's locals a slot in the frame, whichever
22113 /// arm runs. `((((1))))` reaches none of these arms and was carrying
22114 /// five `Expr`-sized locals per level for them anyway.
22115 #[inline(never)]
22116 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22117 self.advance();
22118 let e = self.parse_expr(prec)?;
22119 Ok(Expr::Unary {
22120 op,
22121 expr: Box::new(e),
22122 })
22123 }
22124
22125 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22126 /// and separate from it because of the literal folding below and the
22127 /// `format!` temporaries that folding needs.
22128 #[inline(never)]
22129 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22130 self.advance();
22131 // v7.39 (round 549) — fold the sign into an integer literal that
22132 // only fits once it is negative.
22133 //
22134 // `9223372036854775808` is one past i64::MAX, so the lexer hands
22135 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22136 // folds the sign first, so `-9223372036854775808` is a bigint
22137 // there — and `-9223372036854775808 - 1` raises "bigint out of
22138 // range" where SPG quietly answered -9223372036854775809, a value
22139 // no bigint can hold. The arithmetic itself was already checked;
22140 // only the literal's type was wrong.
22141 if let Token::Numeric(lit) = self.peek()
22142 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22143 {
22144 self.advance();
22145 return Ok(Expr::Literal(Literal::Integer(folded)));
22146 }
22147 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22148 // `<->` slotted into 5 and arithmetic shifted up).
22149 let e = self.parse_expr(9)?;
22150 Ok(Expr::Unary {
22151 op: UnOp::Neg,
22152 expr: Box::new(e),
22153 })
22154 }
22155
22156 /// tsquery `!!` prefix negation, lowered to the catalog function.
22157 /// Binds like unary minus. Out-of-line for the frame reason on
22158 /// `parse_unary_op`.
22159 #[inline(never)]
22160 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22161 self.advance();
22162 let e = self.parse_expr(9)?;
22163 Ok(Expr::FunctionCall {
22164 name: String::from("tsquery_not"),
22165 args: alloc::vec![e],
22166 })
22167 }
22168
22169 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22170 match self.peek() {
22171 // NOT binds tighter than AND / XOR / OR but looser than
22172 // comparisons — its operand takes everything ≥ the comparison
22173 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22174 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22175 // was rung 3, behaviour-identical when 3 was unused; AND now
22176 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22177 Token::Not => self.parse_unary_op(UnOp::Not, 4),
22178 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22179 // The body is out-of-line: `parse_unary` is one of the three
22180 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22181 // inline arm here overflowed the native stack in
22182 // `nesting_budget_errors_cleanly` — the guard test caught it,
22183 // exactly as the eval-side cliff did in rounds 346 and 351.
22184 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22185 self.parse_binary_prefix()
22186 }
22187 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22188 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22189 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22190 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22191 Token::Minus => self.parse_prefix_minus(),
22192 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22193 // worked only because the lexer reads it as one signed literal;
22194 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22195 // PG18 and MariaDB take all of them. Binds like unary minus.
22196 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22197 // Bitwise NOT binds like unary minus.
22198 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22199 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22200 // "center of" operator; desugars to center(x). The whole arm
22201 // is out-of-line: parse_unary sits on the per-nesting-level
22202 // frame chain that MAX_NEST_DEPTH is tuned against, so no
22203 // Expr-sized local may live in this frame.
22204 Token::TsMatch => self.parse_prefix_center(),
22205 // v7.39 (round 508) — the prefix operators that are named
22206 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22207 // is length. Out-of-line for the same nesting-frame reason as
22208 // parse_prefix_center — parse_unary sits on the recursive cycle
22209 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22210 // live in this frame.
22211 Token::At => self.parse_prefix_call("abs"),
22212 Token::Hash => self.parse_prefix_call("npoints"),
22213 Token::AtMinusAt => self.parse_prefix_call("length"),
22214 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22215 // "is horizontal" (lseg / line); desugars to the existing
22216 // isvertical()/ishorizontal() functions. Out-of-line for the
22217 // same nesting-frame reason as parse_prefix_center.
22218 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22219 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22220 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22221 _ => self.parse_atom(),
22222 }
22223 }
22224
22225 /// Parse a parenthesised scalar subquery body after the caller has consumed
22226 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22227 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22228 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22229 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22230 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22231 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22232 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22233 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22234 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22235 /// tips the deep-nesting test into a stack overflow).
22236 #[inline(never)]
22237 fn array_subquery_ahead(&self) -> bool {
22238 if !matches!(self.peek(), Token::LParen) {
22239 return false;
22240 }
22241 matches!(
22242 self.tokens.get(self.pos + 1),
22243 Some(Token::Select | Token::Values)
22244 ) || matches!(
22245 self.tokens.get(self.pos + 1),
22246 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22247 )
22248 }
22249
22250 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22251 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22252 /// locals stay off parse_atom's recursive frame (round 105).
22253 #[inline(never)]
22254 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22255 self.advance(); // consume `[`
22256 let mut items: Vec<Expr> = Vec::new();
22257 if !matches!(self.peek(), Token::RBracket) {
22258 loop {
22259 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22260 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22261 if matches!(self.peek(), Token::LBracket) {
22262 items.push(self.parse_array_bracket_body()?);
22263 } else {
22264 items.push(self.parse_expr(0)?);
22265 }
22266 match self.peek() {
22267 Token::Comma => {
22268 self.advance();
22269 }
22270 Token::RBracket => break,
22271 other => {
22272 return Err(self.err(alloc::format!(
22273 "expected ',' or ']' in ARRAY literal, got {other:?}"
22274 )));
22275 }
22276 }
22277 }
22278 }
22279 self.advance(); // consume `]`
22280 Ok(Expr::Array(items))
22281 }
22282
22283 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22284 /// is already consumed; the current token is `(`. Desugars to a scalar
22285 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22286 /// the subquery's single-column rows in order — reusing the existing
22287 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22288 /// keeps the large `Statement` local off parse_atom's recursive frame.
22289 #[inline(never)]
22290 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22291 self.advance(); // consume `(`
22292 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22293 if w.eq_ignore_ascii_case("with"));
22294 let sub = if is_with {
22295 self.advance(); // WITH
22296 self.parse_with_cte_then_select()?
22297 } else {
22298 self.parse_select_stmt()?
22299 };
22300 if !matches!(self.peek(), Token::RParen) {
22301 return Err(self.err(alloc::format!(
22302 "expected ')' to close ARRAY(subquery), got {:?}",
22303 self.peek()
22304 )));
22305 }
22306 self.advance(); // consume `)`
22307 // Reuse the parser to build the array_agg wrapper from the subquery's
22308 // canonical text — avoids hand-constructing the derived-table AST.
22309 let wrapper = alloc::format!(
22310 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22311 );
22312 let stmt = parse_statement(&wrapper)
22313 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22314 let Statement::Select(sel) = stmt else {
22315 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22316 };
22317 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22318 }
22319
22320 #[inline(never)]
22321 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22322 let inner = if is_with {
22323 self.advance(); // WITH
22324 self.parse_with_cte_then_select()?
22325 } else {
22326 self.parse_select_stmt()?
22327 };
22328 match self.advance() {
22329 Token::RParen => {
22330 let Statement::Select(s) = inner else {
22331 return Err(ParseError {
22332 message: "scalar subquery body must be a SELECT".into(),
22333 token_pos: self.consumed_pos(),
22334 });
22335 };
22336 Ok(Expr::ScalarSubquery(Box::new(s)))
22337 }
22338 other => Err(ParseError {
22339 message: format!("expected ')' after scalar subquery, got {other:?}"),
22340 token_pos: self.consumed_pos(),
22341 }),
22342 }
22343 }
22344
22345 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22346 /// literals. The lexer splits them into an ident + string; recombine
22347 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22348 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22349 /// frame for the `body` / `bits` strings and their char loops (the
22350 /// round-367 frame cliff, M20).
22351 #[inline(never)]
22352 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22353 let is_hex = match self.peek() {
22354 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22355 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22356 _ => return None,
22357 };
22358 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22359 return None;
22360 }
22361 // v7.39.3 — where the LITERAL starts, because the errors below
22362 // are about the literal and both engines point at it. `err`
22363 // reports the CURRENT token, which by then is the one after the
22364 // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22365 // `near '…'` snippet — which runs from the reported position to
22366 // the end — came out empty where MySQL 9.7.2 says `near
22367 // 'x'123''`.
22368 let lit_pos = self.pos;
22369 self.advance();
22370 let Token::String(body) = self.advance() else {
22371 unreachable!("guarded above");
22372 };
22373 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22374 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22375 // (hex pairs, even count required — MariaDB errors on an odd
22376 // count); `b'1010'` packs its bits big-endian, left-padded to a
22377 // byte. Lower both onto the bytea cast.
22378 if self.mysql_dialect {
22379 if is_hex {
22380 if body.len() % 2 == 1 {
22381 return Some(Err(self.err_at(
22382 lit_pos,
22383 alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22384 )));
22385 }
22386 for c in body.chars() {
22387 if !c.is_ascii_hexdigit() {
22388 return Some(Err(self.err_at(
22389 lit_pos,
22390 alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22391 )));
22392 }
22393 }
22394 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22395 }
22396 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22397 return Some(Err(self.err_at(
22398 lit_pos,
22399 alloc::format!("invalid binary digit {bad:?} in b'…'"),
22400 )));
22401 }
22402 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22403 }
22404 let bits = if is_hex {
22405 let mut out = String::with_capacity(body.len() * 4);
22406 for c in body.chars() {
22407 let Some(d) = c.to_digit(16) else {
22408 // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22409 // own quoting: `"g" is not a valid hexadecimal
22410 // digit` (measured, with the caret on the literal).
22411 return Some(Err(self.err_at(
22412 lit_pos,
22413 alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22414 )));
22415 };
22416 out.push_str(&alloc::format!("{d:04b}"));
22417 }
22418 out
22419 } else {
22420 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22421 return Some(Err(self.err_at(
22422 lit_pos,
22423 alloc::format!("\"{bad}\" is not a valid binary digit"),
22424 )));
22425 }
22426 body
22427 };
22428 // Route through the postfix-cast loop so a chained cast like
22429 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22430 // of erroring at the `::`.
22431 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22432 // literal keeps its exact length, while an explicit `::bit` cast is
22433 // bit(1) with pad/truncate semantics (PG).
22434 Some(self.finish_postfix_casts(Expr::Cast {
22435 expr: Box::new(Expr::Literal(Literal::String(bits))),
22436 target: CastTarget::Named("__bit_literal".to_string()),
22437 }))
22438 }
22439
22440 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22441 if let Some(res) = self.try_parse_bit_string_literal() {
22442 return res;
22443 }
22444 let tok_pos = self.pos;
22445 match self.advance() {
22446 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22447 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22448 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22449 // carrying the source mantissa + scale so no precision is lost. A
22450 // literal too wide for i128 falls back to double precision.
22451 // Out-of-line (#[inline(never)]) — this arm sits on the
22452 // parse_expr recursion chain; its expansion locals must not
22453 // widen the recursive frame (debug frame-cliff discipline).
22454 Token::Numeric(s) => match numeric_token_to_literal(s) {
22455 Ok(lit) => Ok(Expr::Literal(lit)),
22456 Err(msg) => Err(self.err(msg)),
22457 },
22458 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22459 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22460 // (the lexer only emits this token in the MySQL dialect). Lower
22461 // onto the existing bytea cast; out-of-line to keep this arm off
22462 // the parse recursion frame.
22463 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22464 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22465 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22466 Token::Null => Ok(Expr::Literal(Literal::Null)),
22467 // v6.1.1 — `$N` placeholder. The actual Value lookup
22468 // happens in the engine eval path against the prepared-
22469 // statement bind buffer.
22470 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22471 Token::LParen => {
22472 // v4.10: `(SELECT ...)` in expression position is a
22473 // scalar subquery; otherwise it's a parenthesised
22474 // expression. Peek for SELECT keyword to dispatch.
22475 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22476 // lexes as Ident("with") (not a reserved token). The subquery body
22477 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22478 // so its large `Statement` local stays out of parse_atom's stack
22479 // frame — parse_atom is on the recursive `((…))` cycle and the
22480 // nesting budget is tuned to its frame size).
22481 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22482 if s.eq_ignore_ascii_case("with"));
22483 if matches!(self.peek(), Token::Select) || is_with {
22484 self.parse_paren_scalar_subquery(is_with)
22485 } else {
22486 let e = self.parse_expr(0)?;
22487 // `(a, b, …)` — a row constructor. Valid only
22488 // in front of a comparison operator or [NOT]
22489 // IN; both expand at parse time (lexicographic
22490 // comparison / OR'd row equalities).
22491 if matches!(self.peek(), Token::Comma) {
22492 let mut row = alloc::vec![e];
22493 while matches!(self.peek(), Token::Comma) {
22494 self.advance();
22495 row.push(self.parse_expr(0)?);
22496 }
22497 if !matches!(self.peek(), Token::RParen) {
22498 return Err(self.err(alloc::format!(
22499 "expected ')' after row constructor, got {:?}",
22500 self.peek()
22501 )));
22502 }
22503 self.advance();
22504 // A bare `(a, b, …)` row constructor can carry postfix
22505 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22506 // early return here skips parse_atom's tail postfix
22507 // pass, so fold casts in explicitly. For the
22508 // comparison / predicate forms nothing postfix follows,
22509 // so this is a no-op.
22510 return self
22511 .parse_row_comparison_tail(row)
22512 .and_then(|e| self.finish_postfix_casts(e));
22513 }
22514 match self.advance() {
22515 Token::RParen => Ok(e),
22516 other => Err(ParseError {
22517 message: format!("expected ')', got {other:?}"),
22518 token_pos: self.consumed_pos(),
22519 }),
22520 }
22521 }
22522 }
22523 Token::LBracket => self.parse_vector_literal_body(),
22524 Token::Extract => self.parse_extract_atom(),
22525 Token::Interval => self.parse_interval_atom(),
22526 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22527 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22528 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22529 // expression position calling the PG `left(string, n)` /
22530 // `right(string, n)` function; rebuild the AST as a regular
22531 // function call so the engine's apply_function dispatch picks
22532 // it up. Delegated to a #[inline(never)] helper so its locals
22533 // don't bloat this recursive `parse_atom` frame (the nesting
22534 // budget in `enter_nested` is tuned to parse_atom's size).
22535 Token::Left if matches!(self.peek(), Token::LParen) => {
22536 self.parse_lr_string_function_call("left")
22537 }
22538 Token::Right if matches!(self.peek(), Token::LParen) => {
22539 self.parse_lr_string_function_call("right")
22540 }
22541 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22542 // token; we match on the bare ident. NOT is a token
22543 // (consumed in the comparison rung), but `EXISTS (...)`
22544 // at the top of an expression starts here.
22545 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22546 self.parse_exists_atom(false)
22547 }
22548 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22549 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22550 // CASE is a bare ident; we dispatch on lowercase match.
22551 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22552 self.parse_case_atom()
22553 }
22554 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22555 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22556 // '…'`. Lower onto the ::cast node so the existing
22557 // runtime text→date/timestamp paths do the parsing. The
22558 // string must follow immediately, else the ident stays a
22559 // plain column reference.
22560 Token::Ident(s)
22561 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22562 && matches!(self.peek(), Token::String(_)) =>
22563 {
22564 let target =
22565 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22566 let Token::String(lit) = self.advance() else {
22567 unreachable!("peek guaranteed a string token");
22568 };
22569 Ok(Expr::Cast {
22570 expr: Box::new(Expr::Literal(Literal::String(lit))),
22571 target,
22572 })
22573 }
22574 // v7.39 (round 221) — the SQL-standard long spellings:
22575 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22576 // TIME ZONE '…'`. Consume the modifier and lower to the same
22577 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22578 Token::Ident(s)
22579 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22580 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22581 || w.eq_ignore_ascii_case("without"))
22582 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22583 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22584 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22585 {
22586 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22587 self.advance(); // WITH / WITHOUT
22588 self.advance(); // TIME
22589 self.advance(); // ZONE
22590 let Token::String(lit) = self.advance() else {
22591 unreachable!("guard checked a string token");
22592 };
22593 let base = s.to_ascii_lowercase();
22594 let target = match (base.as_str(), with_tz) {
22595 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22596 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22597 (_, true) => CastTarget::Timestamptz,
22598 (_, false) => CastTarget::Timestamp,
22599 };
22600 Ok(Expr::Cast {
22601 expr: Box::new(Expr::Literal(Literal::String(lit))),
22602 target,
22603 })
22604 }
22605 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22606 // gathers the subquery's single-column rows (in its row order)
22607 // into an array. Desugared to `array_agg` over the subquery as a
22608 // derived table; out-of-line to keep parse_atom's frame small (it
22609 // sits on the recursive nesting-budget cycle).
22610 Token::Ident(s) | Token::QuotedIdent(s)
22611 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22612 {
22613 self.parse_array_subquery()
22614 }
22615 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22616 // is not a reserved token; we match by case-insensitive
22617 // ident. The opening `[` must follow immediately. v7.39 (read01
22618 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22619 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22620 Token::Ident(s) | Token::QuotedIdent(s)
22621 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22622 {
22623 self.parse_array_literal_body()
22624 }
22625 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22626 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22627 // We special-case before the generic ident dispatch so
22628 // the AGAINST clause never reaches the function-call
22629 // loop (which would mis-read `(cols) AGAINST` as a
22630 // call with no trailing modifier). The shape is
22631 // rewritten to a Boolean OR over per-column
22632 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22633 // term)` so the existing FTS evaluator handles
22634 // semantics — the fulltext-GIN built at CREATE TABLE
22635 // time is currently a "real index that survives dump
22636 // round-trip"; the planner hook that actually uses
22637 // it for posting-list intersection lands in a later
22638 // sub-phase (Phase 2.2b) without touching this surface.
22639 Token::Ident(s) | Token::QuotedIdent(s)
22640 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22641 {
22642 self.parse_match_against_atom()
22643 }
22644 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22645 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22646 // alias names in expression context too. `release` appears
22647 // in sentori `0003_partition_events.sql` as both a column
22648 // reference (SELECT … release …) and an INSERT column list
22649 // entry. Mirrors `expect_ident_like`'s expansion of the
22650 // identifier set.
22651 other if unreserved_keyword_text(&other).is_some() => {
22652 let s = unreserved_keyword_text(&other).unwrap();
22653 self.finish_ident_atom(s)
22654 }
22655 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22656 // only inside `SET` before, so `SELECT @@autocommit` — which
22657 // every MySQL connector asks at handshake — was a parse error.
22658 // MariaDB accepts the bare, `@@session.` and `@@global.`
22659 // spellings alike and answers from the session's own value.
22660 Token::SessionVar(v) => {
22661 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22662 // has nothing to do with a `@@` engine setting: its own
22663 // per-session namespace, and an unset one reads NULL instead
22664 // of raising. Stripping every `@` (as this did) made `@x` and
22665 // `@@x` the same node, so `SELECT @x` answered "Unknown
22666 // system variable".
22667 Ok(variable_ref_atom(&v))
22668 }
22669 other => Err(ParseError {
22670 message: format!("unexpected token {other:?} in expression"),
22671 token_pos: tok_pos,
22672 }),
22673 }
22674 // After parsing the atom, fold any postfix `::vector` casts.
22675 .and_then(|atom| self.finish_postfix_casts(atom))
22676 }
22677
22678 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22679 /// Both bind tighter than any binary op.
22680 /// Shared cast-target parser for postfix `::TYPE` and the
22681 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22682 /// If the next tokens are `( N )`, consume them and return the canonical
22683 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22684 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22685 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22686 if !matches!(self.peek(), Token::LParen) {
22687 return None;
22688 }
22689 self.advance(); // (
22690 let n = match self.advance() {
22691 Token::Integer(n) => n,
22692 _ => return Some(base.to_string()), // malformed → drop precision
22693 };
22694 if matches!(self.peek(), Token::RParen) {
22695 self.advance();
22696 }
22697 Some(alloc::format!("{base}({n})"))
22698 }
22699
22700 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22701 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22702 // schema-qualifies every cast target, and `pg_catalog.X` names
22703 // exactly the builtin type X. Consume the qualifier and let
22704 // the ordinary target parse decide.
22705 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22706 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22707 {
22708 self.advance();
22709 self.advance();
22710 }
22711 let target = match self.advance() {
22712 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22713 "int" | "integer" | "int4" => {
22714 if matches!(self.peek(), Token::LBracket)
22715 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22716 {
22717 self.advance();
22718 self.advance();
22719 CastTarget::IntArray
22720 } else {
22721 CastTarget::Int
22722 }
22723 }
22724 "bigint" | "int8" => {
22725 if matches!(self.peek(), Token::LBracket)
22726 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22727 {
22728 self.advance();
22729 self.advance();
22730 CastTarget::BigIntArray
22731 } else {
22732 CastTarget::BigInt
22733 }
22734 }
22735 "float" | "double" => CastTarget::Float,
22736 "text" => {
22737 // v7.10.11 — `::TEXT[]` widens to TextArray.
22738 if matches!(self.peek(), Token::LBracket)
22739 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22740 {
22741 self.advance();
22742 self.advance();
22743 CastTarget::TextArray
22744 } else {
22745 CastTarget::Text
22746 }
22747 }
22748 "bool" | "boolean" => CastTarget::Bool,
22749 "vector" => CastTarget::Vector,
22750 "date" => CastTarget::Date,
22751 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22752 // seconds precision through the Named path (the engine rounds
22753 // the sub-second field); bare `::timestamp` keeps the fast arm.
22754 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22755 Some(named) => CastTarget::Named(named),
22756 None => CastTarget::Timestamp,
22757 },
22758 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22759 Some(named) => CastTarget::Named(named),
22760 None => CastTarget::Timestamptz,
22761 },
22762 "interval" => CastTarget::Interval,
22763 "json" => CastTarget::Json,
22764 "jsonb" => CastTarget::Jsonb,
22765 // v7.39 (round 694) — these have dedicated CastTarget
22766 // variants, so they never reached the postfix `[]` handling
22767 // further down and `::regtype[]` was a SYNTAX error at the
22768 // `]`. PG has an array type for every scalar; take the
22769 // suffix here and hand the canonical `<ty>_array` name to
22770 // the engine, the same shape every other array cast uses.
22771 "regtype" if self.peek_postfix_array_brackets() => {
22772 self.advance();
22773 self.advance();
22774 CastTarget::Named(alloc::string::String::from("regtype_array"))
22775 }
22776 "regclass" if self.peek_postfix_array_brackets() => {
22777 self.advance();
22778 self.advance();
22779 CastTarget::Named(alloc::string::String::from("regclass_array"))
22780 }
22781 "regtype" => CastTarget::RegType,
22782 "regclass" => CastTarget::RegClass,
22783 // v7.12.0 — `::tsvector` / `::tsquery`.
22784 // Engine decodes the LHS text via the PG
22785 // external form parser.
22786 // v7.39 (round 352, M8) — MySQL's own cast targets.
22787 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22788 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22789 // such type, so they are taken only in that dialect and
22790 // fall through to the "type does not exist" arm otherwise.
22791 "signed" | "unsigned" if self.mysql_dialect => {
22792 if matches!(self.peek(), Token::Ident(k)
22793 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22794 {
22795 self.advance();
22796 }
22797 CastTarget::Named(s.to_ascii_lowercase())
22798 }
22799 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22800 // in MySQL: MariaDB answers '123' where the SQL-standard
22801 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22802 // Truncating a number to its first digit is a wrong answer
22803 // with no error, so the MySQL session gets MySQL's reading.
22804 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22805 CastTarget::Text
22806 }
22807 "tsvector" => CastTarget::TsVector,
22808 "tsquery" => CastTarget::TsQuery,
22809 // v7.17.0 — `::uuid`. Engine decodes the LHS
22810 // text via `spg_storage::parse_uuid_str`.
22811 "uuid" => CastTarget::Uuid,
22812 // v7.18 — `::bytea`. Engine decodes the LHS
22813 // text via the PG hex form (`'\xdeadbeef'`)
22814 // or escape form (`'\\x05\\x00'`). Closes
22815 // mailrs D-pre #3 reverse-acceptance gap.
22816 "bytea" => CastTarget::Bytea,
22817 // v7.37.5 ship triage — generic typed-cast escape.
22818 // Anything the long-tail PG type ident table knows
22819 // about(network/bit/geometry/multirange/etc.)flows
22820 // through `CastTarget::Named(canonical)`; the engine
22821 // resolves via `column_type_to_data_type` and dispatches
22822 // through the typed `coerce_value` path. Truly
22823 // unrecognised idents still hit the error arm below
22824 // because the engine rejects them.
22825 other => {
22826 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22827 // `::varchar(255)`, etc. Capture into the canonical
22828 // `name(p,s)` form so `type_name_to_data_type` can
22829 // reconstruct the `DataType::Numeric { precision,
22830 // scale }` (and similar param-carrying types).
22831 let mut name = other.to_string();
22832 // v7.39 (round 281) — `::bit varying(3)` is two
22833 // words; fold the tail in so the typmod reaches the
22834 // type resolver instead of tripping the parser.
22835 if name.eq_ignore_ascii_case("bit")
22836 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22837 {
22838 self.advance();
22839 name = alloc::string::String::from("varbit");
22840 }
22841 // v7.39 (round 613) — `::character varying` is the same
22842 // two-word shape and had no fold, so the `varying` was
22843 // left behind and the cast became a bare `character`,
22844 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22845 // `a` where PG answers `ab`. Silently, and for a spelling
22846 // pg_dump writes.
22847 if name.eq_ignore_ascii_case("character")
22848 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22849 {
22850 self.advance();
22851 name = alloc::string::String::from("varchar");
22852 }
22853 if matches!(self.peek(), Token::LParen) {
22854 let mut buf = alloc::string::String::from("(");
22855 let mut depth = 0usize;
22856 loop {
22857 match self.advance() {
22858 Token::LParen => {
22859 depth += 1;
22860 if depth > 1 {
22861 buf.push('(');
22862 }
22863 }
22864 Token::RParen => {
22865 depth -= 1;
22866 if depth == 0 {
22867 buf.push(')');
22868 break;
22869 }
22870 buf.push(')');
22871 }
22872 Token::Comma => buf.push(','),
22873 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22874 // v7.39 (round 273) — a minus used to fall
22875 // into the catch-all below and vanish, so
22876 // `::numeric(10,-2)` reached the engine as
22877 // the text `numeric(10,2)` and silently
22878 // rounded to two DECIMALS instead of to
22879 // hundreds. A dropped token is not a
22880 // no-op when it carries a sign.
22881 Token::Minus => buf.push('-'),
22882 Token::Eof => break,
22883 _ => {}
22884 }
22885 }
22886 name.push_str(&buf);
22887 }
22888 // Optional postfix `[]` widens to the array form —
22889 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22890 // The engine's `type_name_to_data_type` recognises
22891 // the canonical `<ty>_array` form.
22892 if matches!(self.peek(), Token::LBracket)
22893 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22894 {
22895 self.advance();
22896 self.advance();
22897 name.push_str("_array");
22898 }
22899 CastTarget::Named(name)
22900 }
22901 },
22902 Token::Interval => CastTarget::Interval,
22903 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22904 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22905 // = char(1)); other quoted names resolve like idents.
22906 Token::QuotedIdent(q) => {
22907 if q.eq_ignore_ascii_case("char") {
22908 CastTarget::Named("char1".into())
22909 } else {
22910 CastTarget::Named(q.to_ascii_lowercase())
22911 }
22912 }
22913 other => {
22914 return Err(ParseError {
22915 message: format!("expected type ident after `::`, got {other:?}"),
22916 token_pos: self.consumed_pos(),
22917 });
22918 }
22919 };
22920 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22921 // target to its array sibling. Closed-enum arms (Bool /
22922 // SmallInt / Numeric / Float / Date / …) didn't carry the
22923 // explicit widening that Text / Int / BigInt did, so
22924 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22925 // error. The widening here mirrors the per-arm Text /
22926 // Int / BigInt logic above + folds the new ζ-A first-class
22927 // types through `CastTarget::Named("<ty>_array")`.
22928 if matches!(self.peek(), Token::LBracket)
22929 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22930 {
22931 let widened = match &target {
22932 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22933 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22934 // v7.39 (round 326, V43) — the two temporal types stay
22935 // distinct. Both used to widen to `timestamptz_array`, so
22936 // `::timestamp[]` named the wrong target in its own error
22937 // message and lost the zone-less identity on the way.
22938 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22939 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22940 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22941 CastTarget::Json | CastTarget::Jsonb => {
22942 Some(CastTarget::Named("jsonb_array".to_string()))
22943 }
22944 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22945 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22946 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22947 CastTarget::Named(name) => {
22948 let mut a = name.clone();
22949 a.push_str("_array");
22950 Some(CastTarget::Named(a))
22951 }
22952 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22953 // RegType / RegClass / TextArray / IntArray /
22954 // BigIntArray already finalised — leave as is.
22955 _ => None,
22956 };
22957 if let Some(w) = widened {
22958 self.advance();
22959 self.advance();
22960 return Ok(w);
22961 }
22962 }
22963 Ok(target)
22964 }
22965
22966 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22967 loop {
22968 // v7.38 (read01, T9) — composite field access `(expr).field`.
22969 // A bare `a.b` is consumed as a qualified column inside the ident
22970 // atom, so a Dot only survives to this postfix position when the
22971 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22972 // `.*` whole-row expansion is not handled here (projection-level).
22973 if matches!(self.peek(), Token::Dot)
22974 && matches!(
22975 self.tokens.get(self.pos + 1),
22976 Some(Token::Ident(_) | Token::QuotedIdent(_))
22977 )
22978 {
22979 self.advance(); // .
22980 let field = match self.advance() {
22981 Token::Ident(s) | Token::QuotedIdent(s) => s,
22982 other => {
22983 return Err(
22984 self.err(format!("expected a field name after '.', got {other:?}"))
22985 );
22986 }
22987 };
22988 expr = Expr::FieldAccess {
22989 base: Box::new(expr),
22990 field,
22991 };
22992 continue;
22993 }
22994 if matches!(self.peek(), Token::DoubleColon) {
22995 self.advance();
22996 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22997 // target set to include INTERVAL (reserved Token),
22998 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22999 // mailrs follow-up H3a + H3b.
23000 let target = self.parse_cast_target()?;
23001 expr = Expr::Cast {
23002 expr: Box::new(expr),
23003 target,
23004 };
23005 continue;
23006 }
23007 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23008 // returns NULL for out-of-range. Multiple subscripts
23009 // chain: `a[i][j]` parses left-to-right.
23010 if matches!(self.peek(), Token::LBracket) {
23011 self.advance();
23012 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23013 // bare index stays a subscript.
23014 let lo = if matches!(self.peek(), Token::Colon) {
23015 None
23016 } else {
23017 Some(self.parse_expr(0)?)
23018 };
23019 if matches!(self.peek(), Token::Colon) {
23020 self.advance();
23021 let hi = if matches!(self.peek(), Token::RBracket) {
23022 None
23023 } else {
23024 Some(Box::new(self.parse_expr(0)?))
23025 };
23026 if !matches!(self.peek(), Token::RBracket) {
23027 return Err(self.err(alloc::format!(
23028 "expected ']' after array slice, got {:?}",
23029 self.peek()
23030 )));
23031 }
23032 self.advance();
23033 expr = Expr::ArraySlice {
23034 target: Box::new(expr),
23035 lo: lo.map(Box::new),
23036 hi,
23037 };
23038 continue;
23039 }
23040 let index = lo.expect("non-colon branch parsed an index");
23041 if !matches!(self.peek(), Token::RBracket) {
23042 return Err(self.err(alloc::format!(
23043 "expected ']' after array index, got {:?}",
23044 self.peek()
23045 )));
23046 }
23047 self.advance();
23048 expr = Expr::ArraySubscript {
23049 target: Box::new(expr),
23050 index: Box::new(index),
23051 };
23052 continue;
23053 }
23054 // `expr AT TIME ZONE zone` — lowers to PG's own function
23055 // form timezone(zone, expr); the scalar implements the
23056 // offset shift (named zones error there — no tzdata).
23057 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23058 && matches!(self.tokens.get(self.pos + 1),
23059 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23060 && matches!(self.tokens.get(self.pos + 2),
23061 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23062 {
23063 self.advance(); // AT
23064 self.advance(); // TIME
23065 self.advance(); // ZONE
23066 // Zone at comparison precedence so AND/OR stay out.
23067 let zone = self.parse_expr(6)?;
23068 expr = Expr::FunctionCall {
23069 name: "timezone".to_string(),
23070 args: alloc::vec![zone, expr],
23071 };
23072 continue;
23073 }
23074 // `expr COLLATE "name"` — SPG's single text ordering IS
23075 // byte order, i.e. the C collation. The byte-order
23076 // spellings absorb as no-ops; a locale collation would
23077 // silently sort differently from PG, so it errors
23078 // honestly instead.
23079 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23080 self.advance();
23081 let mut cname = match self.advance() {
23082 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23083 other => {
23084 return Err(self.err(alloc::format!(
23085 "expected collation name after COLLATE, got {other:?}"
23086 )));
23087 }
23088 };
23089 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23090 // is how `pg_dump` writes the default one:
23091 // `… COLLATE pg_catalog.default`. Reading a single token
23092 // left the SCHEMA as the name, so the clause was refused
23093 // as an unsupported locale collation and no dump ran.
23094 if matches!(self.peek(), Token::Dot) {
23095 // v7.39.2 — the qualifier is DROPPED (SPG is single
23096 // schema) but it is checked first. PostgreSQL 18.6
23097 // answers `schema "nosuch_schema" does not exist` for
23098 // one it has never heard of, and dropping it unread
23099 // meant `COLLATE nosuch_schema."C"` succeeded here —
23100 // a name that names nothing, accepted.
23101 let schema = cname.to_ascii_lowercase();
23102 if !matches!(
23103 schema.as_str(),
23104 "pg_catalog" | "public" | "information_schema"
23105 ) {
23106 return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23107 }
23108 self.advance();
23109 cname = match self.advance() {
23110 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23111 // `default` lexes as a KEYWORD, and it is the name
23112 // pg_dump writes — the same trap round 535 hit with
23113 // TABLE / INDEX / FULL.
23114 Token::Default => alloc::string::String::from("default"),
23115 other => {
23116 return Err(self.err(alloc::format!(
23117 "expected collation name after COLLATE, got {other:?}"
23118 )));
23119 }
23120 };
23121 }
23122 let lc = cname.to_ascii_lowercase();
23123 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23124 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23125 // family / `binary`) forces byte-wise, which is exactly
23126 // what `BINARY expr` does — lower onto that so every fold
23127 // site (comparison, LIKE, ORDER BY) suppresses via
23128 // `is_binary_coerced`. A `_ci` family override folds, and
23129 // under the MySQL dialect the default already folds, so it
23130 // absorbs as a no-op; likewise the C / byte-order spellings.
23131 // v7.39.2 — against MySQL's own list, not against the
23132 // shape of the name. `nosuch_bin` took this shortcut and
23133 // became a BINARY cast; `nosuch_ci` took the one below
23134 // and was absorbed as a no-op. Either way the client
23135 // named a collation that does not exist and was told
23136 // nothing. An unknown name now falls through to the
23137 // node, and the engine refuses it.
23138 let real = crate::charset::is_mysql_collation(&lc);
23139 if self.mysql_dialect && real && (lc.ends_with("_bin") || lc == "binary") {
23140 expr = Expr::Cast {
23141 expr: alloc::boxed::Box::new(expr),
23142 target: CastTarget::Named("binary".to_string()),
23143 };
23144 continue;
23145 }
23146 let mysql_ci = self.mysql_dialect
23147 && ((real && lc.ends_with("_ci"))
23148 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23149 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23150 // goes to the lowering channel, the byte-order spellings
23151 // included. Round 691 recorded only the names the old
23152 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23153 // absorbed as a no-op — and once a column could declare a
23154 // collation, absorbing the clause meant the COLUMN's
23155 // collation won where the query had asked for bytes.
23156 if self.in_order_by_key && !mysql_ci {
23157 self.order_key_collation = Some(cname);
23158 continue;
23159 }
23160 // v7.39.2 — the clause becomes a NODE rather than being
23161 // refused or absorbed.
23162 //
23163 // What stood here refused the locale names and SILENTLY
23164 // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23165 // answered `t` where PostgreSQL 18.6 answers `f`: the one
23166 // family it let through is the one where dropping it
23167 // changes the answer. Absorbing is only correct when the
23168 // collation asked for is the one the comparison would use
23169 // anyway, and that depends on the DATABASE — which the
23170 // parser cannot see. So it rides along and the engine,
23171 // which can, decides.
23172 //
23173 // `collate_derive` already modelled `Explicit(name)` and
23174 // had no way to be handed one.
23175 // v7.39.2 — a MySQL spelling does not exist on the
23176 // PostgreSQL wire, and THIS is where the wire is known.
23177 //
23178 // The check lived in the evaluator first and asked
23179 // `ctx.mysql_dialect`, which the INSERT path builds as a
23180 // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23181 // in a MySQL session was refused for a collation that
23182 // does not exist on a wire it was not on. Making that
23183 // context truthful would change INSERT-time evaluation
23184 // in other ways as a side effect; the parser already
23185 // gates the introducer on the same flag and is the
23186 // honest place to ask.
23187 if !self.mysql_dialect
23188 && (lc.ends_with("_ci")
23189 || lc.ends_with("_cs")
23190 || lc.ends_with("_bin")
23191 || lc == "binary"
23192 || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23193 {
23194 return Err(self.err(alloc::format!(
23195 "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23196 )));
23197 }
23198 // v7.39.3 — the node is built for EVERY name, `_ci`
23199 // included.
23200 //
23201 // A MySQL `_ci` spelling used to be absorbed here on the
23202 // reasoning that a MySQL session folds anyway, so the
23203 // clause asked for what it would have got. That stopped
23204 // being true when the fold learned to read the session's
23205 // collation NAME: under `SET NAMES utf8mb4 COLLATE
23206 // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23207 // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23208 // that would have made it 1 had been dropped in the
23209 // parser. Absorbing is only ever correct when the
23210 // collation asked for is the one the comparison would use
23211 // anyway, and the parser cannot know that — the same
23212 // reasoning already written above for the byte-order
23213 // spellings, applied to the family it had exempted.
23214 expr = Expr::Collate {
23215 expr: alloc::boxed::Box::new(expr),
23216 collation: cname,
23217 };
23218 continue;
23219 }
23220 return Ok(expr);
23221 }
23222 }
23223
23224 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23225 /// the first token that is not one. Schema qualifiers collapse to the
23226 /// last part, which is what every other name path here does (SPG is
23227 /// single-schema).
23228 fn take_comma_separated_names(&mut self) -> Vec<String> {
23229 let mut out = Vec::new();
23230 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23231 self.advance();
23232 let mut last = n;
23233 while matches!(self.peek(), Token::Dot) {
23234 self.advance();
23235 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23236 last = t;
23237 }
23238 }
23239 out.push(last);
23240 if matches!(self.peek(), Token::Comma) {
23241 self.advance();
23242 } else {
23243 break;
23244 }
23245 }
23246 out
23247 }
23248
23249 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23250 ///
23251 /// The general cast-target path tests this inline; the types with their
23252 /// own `CastTarget` variant need it as a guard on their match arm,
23253 /// which is what this exists for.
23254 fn peek_postfix_array_brackets(&self) -> bool {
23255 matches!(self.peek(), Token::LBracket)
23256 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23257 }
23258
23259 /// Parse the operator tail after a `(a, b, …)` row constructor
23260 /// and expand at parse time. `=` is the conjunction of element
23261 /// equalities; `<>` its negation; the order operators expand
23262 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23263 /// equalities. Anything else (a bare row value, a subquery
23264 /// RHS) errors honestly — SPG has no composite runtime value.
23265 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23266 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23267 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23268 lhs: Box::new(l.clone()),
23269 op: BinOp::Eq,
23270 rhs: Box::new(r.clone()),
23271 });
23272 let first = it.next().expect("row has at least two elements");
23273 it.fold(first, |acc, e| Expr::Binary {
23274 lhs: Box::new(acc),
23275 op: BinOp::And,
23276 rhs: Box::new(e),
23277 })
23278 }
23279 // Lexicographic (a,b) OP (c,d):
23280 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
23281 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23282 if lhs.len() == 1 {
23283 return Expr::Binary {
23284 lhs: Box::new(lhs[0].clone()),
23285 op: last,
23286 rhs: Box::new(rhs[0].clone()),
23287 };
23288 }
23289 let head_strict = Expr::Binary {
23290 lhs: Box::new(lhs[0].clone()),
23291 op: strict,
23292 rhs: Box::new(rhs[0].clone()),
23293 };
23294 let head_eq = Expr::Binary {
23295 lhs: Box::new(lhs[0].clone()),
23296 op: BinOp::Eq,
23297 rhs: Box::new(rhs[0].clone()),
23298 };
23299 Expr::Binary {
23300 lhs: Box::new(head_strict),
23301 op: BinOp::Or,
23302 rhs: Box::new(Expr::Binary {
23303 lhs: Box::new(head_eq),
23304 op: BinOp::And,
23305 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23306 }),
23307 }
23308 }
23309 let negated_in = if matches!(self.peek(), Token::Not)
23310 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23311 {
23312 self.advance();
23313 true
23314 } else {
23315 false
23316 };
23317 if matches!(self.peek(), Token::In) {
23318 self.advance();
23319 if !matches!(self.peek(), Token::LParen) {
23320 return Err(self.err(alloc::format!(
23321 "expected '(' after row IN, got {:?}",
23322 self.peek()
23323 )));
23324 }
23325 self.advance();
23326 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23327 // not a list of literal rows. Row-vs-list decomposes to
23328 // OR-of-AND above, but the subquery's rows are only known at
23329 // runtime, so keep it as a RowInSubquery node.
23330 if matches!(self.peek(), Token::Select) {
23331 let inner = self.parse_select_stmt()?;
23332 if !matches!(self.peek(), Token::RParen) {
23333 return Err(self.err(alloc::format!(
23334 "expected ')' after row IN-subquery, got {:?}",
23335 self.peek()
23336 )));
23337 }
23338 self.advance();
23339 let Statement::Select(s) = inner else {
23340 unreachable!("parse_select_stmt always returns Statement::Select")
23341 };
23342 return Ok(Expr::RowInSubquery {
23343 row,
23344 subquery: Box::new(s),
23345 negated: negated_in,
23346 });
23347 }
23348 let mut alternatives: Vec<Expr> = Vec::new();
23349 loop {
23350 // Optional ROW keyword before the paren row.
23351 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23352 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23353 {
23354 self.advance();
23355 }
23356 if !matches!(self.peek(), Token::LParen) {
23357 return Err(self.err(alloc::format!(
23358 "expected '(' to open a row inside IN, got {:?}",
23359 self.peek()
23360 )));
23361 }
23362 self.advance();
23363 let mut rhs = alloc::vec![self.parse_expr(0)?];
23364 while matches!(self.peek(), Token::Comma) {
23365 self.advance();
23366 rhs.push(self.parse_expr(0)?);
23367 }
23368 if !matches!(self.peek(), Token::RParen) {
23369 return Err(self.err(alloc::format!(
23370 "expected ')' after row inside IN, got {:?}",
23371 self.peek()
23372 )));
23373 }
23374 self.advance();
23375 if rhs.len() != row.len() {
23376 return Err(self.err(alloc::format!(
23377 "row IN arity mismatch: left has {}, right has {}",
23378 row.len(),
23379 rhs.len()
23380 )));
23381 }
23382 alternatives.push(row_eq(&row, &rhs));
23383 if matches!(self.peek(), Token::Comma) {
23384 self.advance();
23385 continue;
23386 }
23387 break;
23388 }
23389 if !matches!(self.peek(), Token::RParen) {
23390 return Err(self.err(alloc::format!(
23391 "expected ')' to close row IN list, got {:?}",
23392 self.peek()
23393 )));
23394 }
23395 self.advance();
23396 let mut it = alternatives.into_iter();
23397 let first = it.next().expect("IN list has at least one row");
23398 let combined = it.fold(first, |acc, e| Expr::Binary {
23399 lhs: Box::new(acc),
23400 op: BinOp::Or,
23401 rhs: Box::new(e),
23402 });
23403 return Ok(maybe_not(combined, negated_in));
23404 }
23405 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23406 // two periods share at least one time point. Each pair is
23407 // normalised with least/greatest (PG accepts the endpoints
23408 // in either order), then lowered to the standard
23409 // `start1 < end2 AND start2 < end1` form.
23410 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23411 if row.len() != 2 {
23412 return Err(self.err(alloc::format!(
23413 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23414 row.len()
23415 )));
23416 }
23417 self.advance();
23418 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23419 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23420 {
23421 self.advance();
23422 }
23423 if !matches!(self.peek(), Token::LParen) {
23424 return Err(self.err(alloc::format!(
23425 "expected '(' after OVERLAPS, got {:?}",
23426 self.peek()
23427 )));
23428 }
23429 self.advance();
23430 let r0 = self.parse_expr(0)?;
23431 if !matches!(self.peek(), Token::Comma) {
23432 return Err(self.err(alloc::format!(
23433 "OVERLAPS needs (start, end) on the right, got {:?}",
23434 self.peek()
23435 )));
23436 }
23437 self.advance();
23438 let r1 = self.parse_expr(0)?;
23439 if !matches!(self.peek(), Token::RParen) {
23440 return Err(self.err(alloc::format!(
23441 "expected ')' after OVERLAPS pair, got {:?}",
23442 self.peek()
23443 )));
23444 }
23445 self.advance();
23446 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23447 name: String::from(name),
23448 args: alloc::vec![a.clone(), b.clone()],
23449 };
23450 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23451 lhs: Box::new(lhs),
23452 op: BinOp::Lt,
23453 rhs: Box::new(rhs),
23454 };
23455 return Ok(Expr::Binary {
23456 lhs: Box::new(lt(
23457 pair_fn("least", &row[0], &row[1]),
23458 pair_fn("greatest", &r0, &r1),
23459 )),
23460 op: BinOp::And,
23461 rhs: Box::new(lt(
23462 pair_fn("least", &r0, &r1),
23463 pair_fn("greatest", &row[0], &row[1]),
23464 )),
23465 });
23466 }
23467 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23468 // PG, `IS NULL` is true only when EVERY field is NULL, and
23469 // `IS NOT NULL` is true only when every field is non-NULL — the
23470 // latter is NOT the negation of the former (a mixed row is
23471 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23472 // which reproduces exactly that all-fields semantics.
23473 if matches!(self.peek(), Token::Is) {
23474 self.advance();
23475 let negated = if matches!(self.peek(), Token::Not) {
23476 self.advance();
23477 true
23478 } else {
23479 false
23480 };
23481 if !matches!(self.peek(), Token::Null) {
23482 return Err(self.err(alloc::format!(
23483 "expected NULL after row IS [NOT], got {:?}",
23484 self.peek()
23485 )));
23486 }
23487 self.advance();
23488 let mut it = row.iter().map(|e| Expr::IsNull {
23489 expr: Box::new(e.clone()),
23490 negated,
23491 });
23492 let first = it.next().expect("row has at least two elements");
23493 return Ok(it.fold(first, |acc, e| Expr::Binary {
23494 lhs: Box::new(acc),
23495 op: BinOp::And,
23496 rhs: Box::new(e),
23497 }));
23498 }
23499 let op = match self.peek() {
23500 Token::Eq => BinOp::Eq,
23501 Token::NotEq => BinOp::NotEq,
23502 Token::Lt => BinOp::Lt,
23503 Token::LtEq => BinOp::LtEq,
23504 Token::Gt => BinOp::Gt,
23505 Token::GtEq => BinOp::GtEq,
23506 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23507 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23508 // constructor value, identical to the `ROW(a, b, …)` keyword form:
23509 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23510 // (`::text`, `.field`) applies at the caller just as it does for the
23511 // ROW(...) node. All the comparison / predicate forms returned above.
23512 _ => {
23513 return Ok(Expr::FunctionCall {
23514 name: String::from("row"),
23515 args: row,
23516 });
23517 }
23518 };
23519 self.advance();
23520 // Optional ROW keyword before the paren row.
23521 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23522 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23523 {
23524 self.advance();
23525 }
23526 if !matches!(self.peek(), Token::LParen) {
23527 return Err(self.err(alloc::format!(
23528 "expected '(' to open the right-hand row, got {:?}",
23529 self.peek()
23530 )));
23531 }
23532 self.advance();
23533 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23534 // subquery. Kept as a node (the subquery's row is a runtime value);
23535 // the literal-RHS form below still decomposes at parse time.
23536 if matches!(self.peek(), Token::Select) {
23537 let inner = self.parse_select_stmt()?;
23538 if !matches!(self.peek(), Token::RParen) {
23539 return Err(self.err(alloc::format!(
23540 "expected ')' after row comparison subquery, got {:?}",
23541 self.peek()
23542 )));
23543 }
23544 self.advance();
23545 let Statement::Select(s) = inner else {
23546 unreachable!("parse_select_stmt always returns Statement::Select")
23547 };
23548 return Ok(Expr::RowCmpSubquery {
23549 row,
23550 op,
23551 subquery: Box::new(s),
23552 });
23553 }
23554 let mut rhs = alloc::vec![self.parse_expr(0)?];
23555 while matches!(self.peek(), Token::Comma) {
23556 self.advance();
23557 rhs.push(self.parse_expr(0)?);
23558 }
23559 if !matches!(self.peek(), Token::RParen) {
23560 return Err(self.err(alloc::format!(
23561 "expected ')' after right-hand row, got {:?}",
23562 self.peek()
23563 )));
23564 }
23565 self.advance();
23566 if rhs.len() != row.len() {
23567 // v7.39 (round 239) — PG's wording (42601).
23568 return Err(self.err("unequal number of entries in row expressions".to_string()));
23569 }
23570 Ok(match op {
23571 BinOp::Eq => row_eq(&row, &rhs),
23572 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23573 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23574 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23575 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23576 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23577 _ => unreachable!("op restricted above"),
23578 })
23579 }
23580
23581 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23582 /// escape character becomes the matcher's default backslash:
23583 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23584 /// → the char itself, and any pre-existing backslash escapes
23585 /// itself so it stays literal. Both operands must be string
23586 /// literals — a runtime pattern would need matcher support.
23587 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23588 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23589 (&pattern, &esc)
23590 else {
23591 return Err(
23592 "LIKE ... ESCAPE requires string-literal pattern and escape \
23593 (runtime escape characters are not supported yet)"
23594 .into(),
23595 );
23596 };
23597 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23598 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23599 // multi-character escape is an error.
23600 let esc_ch: Option<char> = {
23601 let mut ch_iter = e.chars();
23602 match (ch_iter.next(), ch_iter.next()) {
23603 (Some(c), None) => Some(c),
23604 (None, _) => None,
23605 (Some(_), Some(_)) => {
23606 return Err(alloc::format!(
23607 "ESCAPE must be a single character, got {e:?}"
23608 ));
23609 }
23610 }
23611 };
23612 let mut out = String::with_capacity(p.len() + 4);
23613 let mut chars = p.chars();
23614 while let Some(c) = chars.next() {
23615 if Some(c) == esc_ch {
23616 match chars.next() {
23617 // Escaped wildcard / escaped escape → keep the
23618 // next char literal via backslash.
23619 Some(next) => {
23620 out.push('\\');
23621 out.push(next);
23622 }
23623 None => {
23624 return Err("LIKE pattern ends with the escape character".into());
23625 }
23626 }
23627 } else if c == '\\' && esc_ch != Some('\\') {
23628 // A raw backslash is literal under a custom (or absent) escape
23629 // — escape it for the backslash-based matcher.
23630 out.push('\\');
23631 out.push('\\');
23632 } else {
23633 out.push(c);
23634 }
23635 }
23636 Ok(Expr::Literal(Literal::String(out)))
23637 }
23638
23639 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23640 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23641 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23642 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23643 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23644 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23645 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23646 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23647 /// array expression errors honestly rather than silently mismatching.
23648 fn try_like_any_all(
23649 &mut self,
23650 base: &Expr,
23651 negated: bool,
23652 case_insensitive: bool,
23653 ) -> Result<Option<Expr>, ParseError> {
23654 let is_any = match self.peek() {
23655 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23656 Token::Ident(s)
23657 if s.eq_ignore_ascii_case("any")
23658 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23659 {
23660 true
23661 }
23662 _ => return Ok(None),
23663 };
23664 self.advance(); // ANY / ALL
23665 self.advance(); // '('
23666 let arr = self.parse_expr(0)?;
23667 if !matches!(self.peek(), Token::RParen) {
23668 return Err(self.err(format!(
23669 "expected ')' after LIKE {} argument, got {:?}",
23670 if is_any { "ANY" } else { "ALL" },
23671 self.peek()
23672 )));
23673 }
23674 self.advance(); // ')'
23675 let Expr::Array(items) = arr else {
23676 return Err(self.err(
23677 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23678 ));
23679 };
23680 let mut clauses = items.into_iter().map(|p| Expr::Like {
23681 expr: Box::new(base.clone()),
23682 pattern: Box::new(p),
23683 negated,
23684 case_insensitive,
23685 });
23686 let Some(first) = clauses.next() else {
23687 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23688 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23689 };
23690 let op = if is_any { BinOp::Or } else { BinOp::And };
23691 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23692 lhs: Box::new(acc),
23693 op,
23694 rhs: Box::new(c),
23695 });
23696 Ok(Some(combined))
23697 }
23698
23699 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23700 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23701 /// `AND` is not swallowed.
23702 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23703 self.advance(); // BETWEEN
23704 // SYMMETRIC — the bounds may arrive in either order; both
23705 // orientations OR together. ASYMMETRIC is the default and
23706 // absorbs as noise.
23707 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23708 {
23709 self.advance();
23710 true
23711 } else {
23712 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23713 self.advance();
23714 }
23715 false
23716 };
23717 let low = self.parse_expr(6)?;
23718 if !matches!(self.peek(), Token::And) {
23719 return Err(self.err(format!(
23720 "expected AND after BETWEEN low bound, got {:?}",
23721 self.peek()
23722 )));
23723 }
23724 self.advance();
23725 let high = self.parse_expr(6)?;
23726 let target = Box::new(expr);
23727 let range = |lo: Expr, hi: Expr| Expr::Binary {
23728 lhs: Box::new(Expr::Binary {
23729 lhs: target.clone(),
23730 op: BinOp::GtEq,
23731 rhs: Box::new(lo),
23732 }),
23733 op: BinOp::And,
23734 rhs: Box::new(Expr::Binary {
23735 lhs: target.clone(),
23736 op: BinOp::LtEq,
23737 rhs: Box::new(hi),
23738 }),
23739 };
23740 let combined = if symmetric {
23741 Expr::Binary {
23742 lhs: Box::new(range(low.clone(), high.clone())),
23743 op: BinOp::Or,
23744 rhs: Box::new(range(high, low)),
23745 }
23746 } else {
23747 range(low, high)
23748 };
23749 Ok(maybe_not(combined, negated))
23750 }
23751
23752 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23753 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23754 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23755 /// Caller already consumed the leading `WITH` ident.
23756 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23757 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23758 /// self-reference that appears more than once in a single term.
23759 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23760 use crate::ast::{CteBody, SelectStatement};
23761 if !cte.recursive {
23762 return Ok(());
23763 }
23764 let CteBody::Select(body) = &cte.body else {
23765 return Ok(());
23766 };
23767 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23768 // check the anchor and every peer term.
23769 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23770 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23771 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23772 return Err(self.err(String::from(
23773 "ORDER BY in a recursive query is not implemented",
23774 )));
23775 }
23776 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23777 return Err(self.err(String::from(
23778 "LIMIT in a recursive query is not implemented",
23779 )));
23780 }
23781 let self_refs = |s: &SelectStatement| -> usize {
23782 let Some(from) = &s.from else {
23783 return 0;
23784 };
23785 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23786 for j in &from.joins {
23787 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23788 n += 1;
23789 }
23790 }
23791 n
23792 };
23793 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23794 return Err(self.err(alloc::format!(
23795 "recursive reference to query \"{}\" must not appear more than once",
23796 cte.name
23797 )));
23798 }
23799 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23800 // apply only when the body actually references itself (a non-self-
23801 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23802 let anchor_refs = self_refs(body);
23803 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23804 if anchor_refs > 0 || union_refs {
23805 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23806 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23807 // "does not have the form" error — SPG used to compute a value.
23808 if body.unions.is_empty()
23809 || body.unions.iter().any(|(k, _)| {
23810 !matches!(
23811 k,
23812 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23813 )
23814 })
23815 {
23816 return Err(self.err(alloc::format!(
23817 "recursive query \"{}\" does not have the form non-recursive-term \
23818 UNION [ALL] recursive-term",
23819 cte.name
23820 )));
23821 }
23822 if anchor_refs > 0 {
23823 return Err(self.err(alloc::format!(
23824 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23825 cte.name
23826 )));
23827 }
23828 }
23829 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23830 for (_, u) in &body.unions {
23831 if self_refs(u) == 0 {
23832 continue;
23833 }
23834 // The self-reference must not sit on the nullable side of an outer
23835 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23836 if let Some(from) = &u.from {
23837 for (i, j) in from.joins.iter().enumerate() {
23838 let left_has_self = is_self(&from.primary)
23839 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23840 let violated = match j.kind {
23841 crate::ast::JoinKind::Left => is_self(&j.table),
23842 crate::ast::JoinKind::Right => left_has_self,
23843 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23844 _ => false,
23845 };
23846 if violated {
23847 return Err(self.err(alloc::format!(
23848 "recursive reference to query \"{}\" must not appear within an outer join",
23849 cte.name
23850 )));
23851 }
23852 }
23853 }
23854 // No aggregates at the top level of the recursive term (SPG used
23855 // to run them and surface a misleading downstream error).
23856 let mut items_and_having: Vec<&Expr> = Vec::new();
23857 for it in &u.items {
23858 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23859 items_and_having.push(expr);
23860 }
23861 }
23862 if let Some(h) = &u.having {
23863 items_and_having.push(h);
23864 }
23865 for e in items_and_having {
23866 if expr_has_toplevel_aggregate(e) {
23867 return Err(self.err(String::from(
23868 "aggregate functions are not allowed in a recursive query's recursive term",
23869 )));
23870 }
23871 }
23872 }
23873 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23874 // subquery) anywhere in the body is rejected; a plain FROM derived
23875 // table is legal in PG and untouched here.
23876 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23877 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23878 for term in all_terms {
23879 if select_has_self_ref_in_sublink(term, &cte.name) {
23880 return Err(self.err(alloc::format!(
23881 "recursive reference to query \"{}\" must not appear within a subquery",
23882 cte.name
23883 )));
23884 }
23885 }
23886 Ok(())
23887 }
23888
23889 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23890 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23891 /// right after parse so the engine sees a plain recursive CTE with the
23892 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23893 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23894 /// text-rendered rows can't provide, and errors honestly.
23895 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23896 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23897 if cte.search.is_none() && cte.cycle.is_none() {
23898 return Ok(());
23899 }
23900 let cte_name = cte.name.clone();
23901 let col_names = cte.column_overrides.clone();
23902 if col_names.is_empty() {
23903 return Err(
23904 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23905 );
23906 }
23907 let search = cte.search.take();
23908 let cycle = cte.cycle.take();
23909 let mut extra_cols: Vec<String> = Vec::new();
23910 let col_ref = |name: &str| {
23911 Expr::Column(ColumnName {
23912 qualifier: Some(cte_name.clone()),
23913 name: name.to_string(),
23914 })
23915 };
23916 // Position of a SEARCH/CYCLE column within the CTE's column list.
23917 let pos_of = |name: &str| -> Result<usize, ParseError> {
23918 col_names
23919 .iter()
23920 .position(|c| c.eq_ignore_ascii_case(name))
23921 .ok_or_else(|| {
23922 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23923 })
23924 };
23925 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23926 let mut args = Vec::with_capacity(positions.len());
23927 for &p in positions {
23928 match items.get(p) {
23929 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23930 _ => {
23931 return Err(self.err(
23932 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23933 ));
23934 }
23935 }
23936 }
23937 Ok(Expr::FunctionCall {
23938 name: "row".into(),
23939 args,
23940 })
23941 };
23942 let CteBody::Select(body) = &mut cte.body else {
23943 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23944 };
23945 if body.unions.is_empty() {
23946 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23947 }
23948 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23949
23950 if let Some(srch) = search {
23951 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23952 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23953 // no typed `record[]`, but element-wise array ORDER BY is correct
23954 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23955 // exactly onto a typed array: DEPTH is the root→node path
23956 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23957 // orders numerically (multi-digit keys included), matching PG.
23958 //
23959 // A multi-column BY would need a record[] to keep the per-node key
23960 // tuple orderable, which SPG can't express — error honestly there
23961 // rather than mis-order.
23962 if srch.by_columns.len() != 1 {
23963 return Err(self.err(
23964 "SEARCH … BY with multiple columns needs typed record[] ordering \
23965 SPG doesn't have yet; a single BY column is supported"
23966 .into(),
23967 ));
23968 }
23969 let key_pos = pos_of(&srch.by_columns[0])?;
23970 let base_key = match body.items.get(key_pos) {
23971 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23972 _ => {
23973 return Err(
23974 self.err("SEARCH BY column maps to a non-expression select item".into())
23975 );
23976 }
23977 };
23978 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23979 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23980 _ => {
23981 return Err(
23982 self.err("SEARCH BY column maps to a non-expression select item".into())
23983 );
23984 }
23985 };
23986 if srch.depth_first {
23987 // base: ARRAY[key]; rec: array_append(cte.set, key).
23988 body.items.push(SelectItem::Expr {
23989 expr: Expr::Array(alloc::vec![base_key]),
23990 alias: Some(srch.set_column.clone()),
23991 });
23992 body.unions[rec].1.items.push(SelectItem::Expr {
23993 expr: Expr::FunctionCall {
23994 name: "array_append".into(),
23995 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23996 },
23997 alias: Some(srch.set_column.clone()),
23998 });
23999 } else {
24000 // BREADTH: [depth, key]; depth starts at 0 and increments. The
24001 // leading depth element dominates the element-wise comparison,
24002 // so shallower rows sort first, then by key — PG's (depth, key).
24003 body.items.push(SelectItem::Expr {
24004 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24005 alias: Some(srch.set_column.clone()),
24006 });
24007 // rec depth = cte.set[1] + 1.
24008 let parent_depth = Expr::ArraySubscript {
24009 target: Box::new(col_ref(&srch.set_column)),
24010 index: Box::new(Expr::Literal(Literal::Integer(1))),
24011 };
24012 body.unions[rec].1.items.push(SelectItem::Expr {
24013 expr: Expr::Array(alloc::vec![
24014 Expr::Binary {
24015 lhs: Box::new(parent_depth),
24016 op: BinOp::Add,
24017 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24018 },
24019 rec_key,
24020 ]),
24021 alias: Some(srch.set_column.clone()),
24022 });
24023 }
24024 extra_cols.push(srch.set_column);
24025 }
24026
24027 if let Some(cyc) = cycle {
24028 let positions: Vec<usize> = cyc
24029 .columns
24030 .iter()
24031 .map(|c| pos_of(c))
24032 .collect::<Result<_, _>>()?;
24033 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24034 // cast it to text for the cycle path: membership only needs equality,
24035 // and the record text form gives SPG a TextArray path (SPG has no
24036 // typed record[] array). Cycle detection is unaffected.
24037 let base_row = Expr::Cast {
24038 expr: Box::new(row_of(&body.items, &positions)?),
24039 target: CastTarget::Text,
24040 };
24041 let rec_row = Expr::Cast {
24042 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24043 target: CastTarget::Text,
24044 };
24045 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24046 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24047 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24048 body.items.push(SelectItem::Expr {
24049 expr: Expr::Literal(dflt.clone()),
24050 alias: Some(cyc.mark_column.clone()),
24051 });
24052 body.items.push(SelectItem::Expr {
24053 expr: Expr::Array(alloc::vec![base_row]),
24054 alias: Some(cyc.path_column.clone()),
24055 });
24056 // rec mark: ROW(cols) already in the path → cycle.
24057 let hit = Expr::AnyAll {
24058 expr: Box::new(rec_row.clone()),
24059 op: BinOp::Eq,
24060 array: Box::new(col_ref(&cyc.path_column)),
24061 is_any: true,
24062 };
24063 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24064 Expr::Case {
24065 operand: None,
24066 branches: alloc::vec![(hit, Expr::Literal(mark))],
24067 else_branch: Some(Box::new(Expr::Literal(dflt))),
24068 }
24069 } else {
24070 hit
24071 };
24072 body.unions[rec].1.items.push(SelectItem::Expr {
24073 expr: mark_expr,
24074 alias: Some(cyc.mark_column.clone()),
24075 });
24076 // rec path: array_append(cte.path, ROW(cols)).
24077 body.unions[rec].1.items.push(SelectItem::Expr {
24078 expr: Expr::FunctionCall {
24079 name: "array_append".into(),
24080 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24081 },
24082 alias: Some(cyc.path_column.clone()),
24083 });
24084 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24085 let stop = Expr::Unary {
24086 op: UnOp::Not,
24087 expr: Box::new(col_ref(&cyc.mark_column)),
24088 };
24089 let w = &mut body.unions[rec].1.where_;
24090 *w = Some(match w.take() {
24091 Some(prev) => Expr::Binary {
24092 lhs: Box::new(prev),
24093 op: BinOp::And,
24094 rhs: Box::new(stop),
24095 },
24096 None => stop,
24097 });
24098 extra_cols.push(cyc.mark_column);
24099 extra_cols.push(cyc.path_column);
24100 }
24101 cte.column_overrides.extend(extra_cols);
24102 Ok(())
24103 }
24104
24105 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24106 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24107 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24108 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24109 return Ok(None);
24110 }
24111 self.advance(); // SEARCH
24112 let depth_first = match self.peek() {
24113 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24114 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24115 other => {
24116 return Err(self.err(format!(
24117 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24118 )));
24119 }
24120 };
24121 self.advance();
24122 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24123 return Err(self.err(format!(
24124 "expected FIRST after SEARCH mode, got {:?}",
24125 self.peek()
24126 )));
24127 }
24128 self.advance();
24129 if !self.peek_is_by() {
24130 return Err(self.err(format!(
24131 "expected BY after SEARCH … FIRST, got {:?}",
24132 self.peek()
24133 )));
24134 }
24135 self.advance();
24136 let mut by_columns = alloc::vec![self.expect_ident_like()?];
24137 while matches!(self.peek(), Token::Comma) {
24138 self.advance();
24139 by_columns.push(self.expect_ident_like()?);
24140 }
24141 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24142 return Err(self.err(format!(
24143 "expected SET in SEARCH clause, got {:?}",
24144 self.peek()
24145 )));
24146 }
24147 self.advance();
24148 let set_column = self.expect_ident_like()?;
24149 Ok(Some(crate::ast::SearchClause {
24150 depth_first,
24151 by_columns,
24152 set_column,
24153 }))
24154 }
24155
24156 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24157 /// USING pathcol`. Returns None when the next token isn't CYCLE.
24158 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24159 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24160 return Ok(None);
24161 }
24162 self.advance(); // CYCLE
24163 let mut columns = alloc::vec![self.expect_ident_like()?];
24164 while matches!(self.peek(), Token::Comma) {
24165 self.advance();
24166 columns.push(self.expect_ident_like()?);
24167 }
24168 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24169 return Err(self.err(format!(
24170 "expected SET in CYCLE clause, got {:?}",
24171 self.peek()
24172 )));
24173 }
24174 self.advance();
24175 let mark_column = self.expect_ident_like()?;
24176 let mut mark_value = None;
24177 let mut default_value = None;
24178 if matches!(self.peek(), Token::To) {
24179 self.advance();
24180 mark_value = Some(self.parse_cycle_literal()?);
24181 if !matches!(self.peek(), Token::Default) {
24182 return Err(self.err(format!(
24183 "expected DEFAULT after CYCLE … TO, got {:?}",
24184 self.peek()
24185 )));
24186 }
24187 self.advance();
24188 default_value = Some(self.parse_cycle_literal()?);
24189 }
24190 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24191 return Err(self.err(format!(
24192 "expected USING in CYCLE clause, got {:?}",
24193 self.peek()
24194 )));
24195 }
24196 self.advance();
24197 let path_column = self.expect_ident_like()?;
24198 Ok(Some(crate::ast::CycleClause {
24199 columns,
24200 mark_column,
24201 mark_value,
24202 default_value,
24203 path_column,
24204 }))
24205 }
24206
24207 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24208 /// literal (string / bool / number) in PG.
24209 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24210 match self.parse_expr(0)? {
24211 Expr::Literal(l) => Ok(l),
24212 other => Err(self.err(format!(
24213 "CYCLE mark/default value must be a literal, got {other:?}"
24214 ))),
24215 }
24216 }
24217
24218 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24219 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24220 // Comes through as an identifier; consume it if present and
24221 // mark every CTE in the clause as recursive (PG semantics —
24222 // the flag is per-WITH, not per-CTE).
24223 let mut recursive = false;
24224 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24225 && s.eq_ignore_ascii_case("recursive")
24226 {
24227 self.advance();
24228 recursive = true;
24229 }
24230 let mut ctes = Vec::new();
24231 loop {
24232 let name = self.expect_ident_like()?;
24233 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24234 // PG uses these to rename the body's output columns; we
24235 // do the same below by overriding `columns[i].name`.
24236 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24237 self.advance();
24238 let mut names = Vec::new();
24239 loop {
24240 names.push(self.expect_ident_like()?);
24241 if matches!(self.peek(), Token::Comma) {
24242 self.advance();
24243 continue;
24244 }
24245 break;
24246 }
24247 if !matches!(self.peek(), Token::RParen) {
24248 return Err(self.err(format!(
24249 "expected ')' to close CTE column list, got {:?}",
24250 self.peek()
24251 )));
24252 }
24253 self.advance();
24254 names
24255 } else {
24256 Vec::new()
24257 };
24258 // AS is a reserved Token::As (used by SELECT-item / FROM
24259 // aliasing) — handle it specially rather than as a bare
24260 // ident.
24261 if !matches!(self.peek(), Token::As) {
24262 return Err(self.err(format!(
24263 "expected AS after CTE name {name:?}, got {:?}",
24264 self.peek()
24265 )));
24266 }
24267 self.advance();
24268 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24269 // MATERIALIZED` optimizer hints. SPG materialises every
24270 // CTE, so both spellings are accepted and absorbed.
24271 if matches!(self.peek(), Token::Not) {
24272 self.advance(); // NOT
24273 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24274 if s.eq_ignore_ascii_case("materialized"))
24275 {
24276 self.advance();
24277 } else {
24278 return Err(self.err(format!(
24279 "expected MATERIALIZED after AS NOT, got {:?}",
24280 self.peek()
24281 )));
24282 }
24283 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24284 if s.eq_ignore_ascii_case("materialized"))
24285 {
24286 self.advance();
24287 }
24288 if !matches!(self.peek(), Token::LParen) {
24289 return Err(self.err(format!(
24290 "expected '(' after AS in WITH clause, got {:?}",
24291 self.peek()
24292 )));
24293 }
24294 self.advance();
24295 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24296 // RETURNING) as the CTE body in addition to SELECT.
24297 // PG writable CTE semantics. UPDATE / DELETE come in as
24298 // bare Idents (lexer keeps SELECT / INSERT as reserved
24299 // tokens but treats the rest of DML as case-insensitive
24300 // idents).
24301 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24302 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24303 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24304 let body = match self.peek() {
24305 Token::Select => {
24306 let inner = self.parse_select_stmt()?;
24307 let Statement::Select(s) = inner else {
24308 unreachable!("parse_select_stmt returns Select");
24309 };
24310 crate::ast::CteBody::Select(s)
24311 }
24312 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24313 // `SELECT * FROM t` this way and accepts it wherever a
24314 // SELECT goes, so the CTE body dispatch needs its own
24315 // arm: this match is keyed on the FIRST token, and
24316 // `Token::Table` fell through to a tail that then
24317 // rejected what it got. `parse_table_shorthand` has
24318 // returned a desugared SelectStatement since the
24319 // shorthand landed — only the routing was missing.
24320 // Round 868 found this by putting the shorthand in a
24321 // subquery; every earlier check used a top-level form.
24322 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24323 // `SELECT * FROM t` this way and accepts it wherever a
24324 // SELECT goes, so the CTE body dispatch needs its own
24325 // arm: this match is keyed on the FIRST token, and
24326 // `Token::Table` fell through to a tail that rejected
24327 // what it got. `parse_table_shorthand` has returned a
24328 // desugared SelectStatement since the shorthand landed —
24329 // only the routing was missing, here and in the derived
24330 // table's second-token gate. Round 868 found both by
24331 // putting the shorthand in a subquery; every earlier
24332 // check had used a top-level form.
24333 Token::Table
24334 if matches!(
24335 self.tokens.get(self.pos + 1),
24336 Some(Token::Ident(_) | Token::QuotedIdent(_))
24337 ) =>
24338 {
24339 let mut head = self.parse_table_shorthand()?;
24340 self.parse_setop_chain_into(&mut head)?;
24341 self.parse_select_tail_into(&mut head)?;
24342 crate::ast::CteBody::Select(head)
24343 }
24344 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24345 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24346 // the shared rows helper onto a Select body.
24347 Token::Values => {
24348 self.advance(); // VALUES
24349 let mut head = self.parse_values_rows_body()?;
24350 // A VALUES seed can head a set-operation chain —
24351 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24352 // SELECT n+1 FROM t …). Attach any trailing
24353 // UNION / INTERSECT / EXCEPT peers so the
24354 // recursive-CTE body parses like the SELECT seed.
24355 self.parse_setop_chain_into(&mut head)?;
24356 crate::ast::CteBody::Select(head)
24357 }
24358 Token::Insert => {
24359 let inner = self.parse_one_statement()?;
24360 let Statement::Insert(s) = inner else {
24361 unreachable!("Token::Insert routes to Insert");
24362 };
24363 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24364 }
24365 _ if is_update_kw => {
24366 let inner = self.parse_one_statement()?;
24367 let Statement::Update(s) = inner else {
24368 return Err(
24369 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24370 );
24371 };
24372 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24373 }
24374 _ if is_delete_kw => {
24375 let inner = self.parse_one_statement()?;
24376 let Statement::Delete(s) = inner else {
24377 return Err(
24378 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24379 );
24380 };
24381 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24382 }
24383 // v7.39 (round 149) — PG 17 allows MERGE as a
24384 // data-modifying CTE body.
24385 _ if is_merge_kw => {
24386 let inner = self.parse_one_statement()?;
24387 let Statement::Merge(s) = inner else {
24388 return Err(
24389 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24390 );
24391 };
24392 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24393 }
24394 // v7.39 (round 151) — a CTE body may itself be
24395 // WITH-headed (PG grammar: PreparableStmt carries its
24396 // own with_clause). The nested statement keeps its own
24397 // ctes; the modifying-CTE-at-top-level rule is enforced
24398 // at execution.
24399 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24400 self.advance(); // WITH
24401 match self.parse_with_cte_then_select()? {
24402 Statement::Select(s) => crate::ast::CteBody::Select(s),
24403 Statement::Insert(s) => {
24404 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24405 }
24406 Statement::Update(s) => {
24407 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24408 }
24409 Statement::Delete(s) => {
24410 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24411 }
24412 Statement::Merge(s) => {
24413 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24414 }
24415
24416 other => {
24417 return Err(self.err(format!(
24418 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24419 )));
24420 }
24421 }
24422 }
24423 other => {
24424 return Err(self.err(format!(
24425 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24426 )));
24427 }
24428 };
24429 if !matches!(self.peek(), Token::RParen) {
24430 return Err(self.err(format!(
24431 "expected ')' after CTE body, got {:?}",
24432 self.peek()
24433 )));
24434 }
24435 self.advance();
24436 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24437 // CTE, desugared into extra body columns by the engine.
24438 let search = self.parse_cte_search_clause()?;
24439 let cycle = self.parse_cte_cycle_clause()?;
24440 let mut cte = crate::ast::Cte {
24441 name,
24442 body,
24443 recursive,
24444 column_overrides,
24445 search,
24446 cycle,
24447 };
24448 self.validate_recursive_cte(&cte)?;
24449 self.desugar_cte_search_cycle(&mut cte)?;
24450 ctes.push(cte);
24451 if matches!(self.peek(), Token::Comma) {
24452 self.advance();
24453 continue;
24454 }
24455 break;
24456 }
24457 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24458 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24459 // the parsed CTEs to whichever statement the body produces.
24460 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24461 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24462 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24463 match self.peek() {
24464 Token::Select => {
24465 let body_stmt = self.parse_select_stmt()?;
24466 let Statement::Select(mut body) = body_stmt else {
24467 unreachable!()
24468 };
24469 body.ctes = ctes;
24470 Ok(Statement::Select(body))
24471 }
24472 Token::Insert => {
24473 let body_stmt = self.parse_one_statement()?;
24474 let Statement::Insert(mut body) = body_stmt else {
24475 unreachable!()
24476 };
24477 body.ctes = ctes;
24478 Ok(Statement::Insert(body))
24479 }
24480 _ if outer_is_update => {
24481 let body_stmt = self.parse_one_statement()?;
24482 let Statement::Update(mut body) = body_stmt else {
24483 return Err(self.err(format!("expected UPDATE after WITH clause")));
24484 };
24485 body.ctes = ctes;
24486 Ok(Statement::Update(body))
24487 }
24488 _ if outer_is_delete => {
24489 let body_stmt = self.parse_one_statement()?;
24490 let Statement::Delete(mut body) = body_stmt else {
24491 return Err(self.err(format!("expected DELETE after WITH clause")));
24492 };
24493 body.ctes = ctes;
24494 Ok(Statement::Delete(body))
24495 }
24496 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24497 // WITH RECURSIVE is rejected with PG's exact message
24498 // (parse analysis, transformWithClause).
24499 _ if outer_is_merge => {
24500 if recursive {
24501 return Err(self.err(String::from(
24502 "WITH RECURSIVE is not supported for MERGE statement",
24503 )));
24504 }
24505 let body_stmt = self.parse_one_statement()?;
24506 let Statement::Merge(mut body) = body_stmt else {
24507 return Err(self.err(format!("expected MERGE after WITH clause")));
24508 };
24509 body.ctes = ctes;
24510 Ok(Statement::Merge(body))
24511 }
24512 other => Err(self.err(format!(
24513 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24514 ))),
24515 }
24516 }
24517
24518 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24519 /// already consumed the leading `EXISTS` ident via
24520 /// `self.advance()`.
24521 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24522 /// the leading `CASE` ident has been consumed (mailrs round-5
24523 /// G9). Supports both the searched form
24524 /// (`CASE WHEN cond THEN val …`) and the simple form
24525 /// (`CASE operand WHEN val THEN val …`).
24526 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24527 // Disambiguate searched vs simple form: if the next token
24528 // is `WHEN`, we're in the searched form. Otherwise the
24529 // intervening expression is the operand.
24530 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24531 None
24532 } else {
24533 Some(Box::new(self.parse_expr(0)?))
24534 };
24535 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24536 loop {
24537 match self.peek() {
24538 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24539 self.advance();
24540 let cond = self.parse_expr(0)?;
24541 match self.peek() {
24542 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24543 self.advance();
24544 }
24545 other => {
24546 return Err(self.err(alloc::format!(
24547 "expected THEN after CASE WHEN <expr>, got {other:?}"
24548 )));
24549 }
24550 }
24551 let value = self.parse_expr(0)?;
24552 branches.push((cond, value));
24553 }
24554 _ => break,
24555 }
24556 }
24557 if branches.is_empty() {
24558 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24559 }
24560 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24561 {
24562 self.advance();
24563 Some(Box::new(self.parse_expr(0)?))
24564 } else {
24565 None
24566 };
24567 match self.peek() {
24568 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24569 self.advance();
24570 }
24571 other => {
24572 return Err(self.err(alloc::format!(
24573 "expected END to close CASE expression, got {other:?}"
24574 )));
24575 }
24576 }
24577 Ok(Expr::Case {
24578 operand,
24579 branches,
24580 else_branch,
24581 })
24582 }
24583
24584 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24585 /// query-source position (EXISTS / IN / INSERT source / CTE body /
24586 /// view body). Caller consumed the WITH keyword. Only a SELECT
24587 /// outer is grammatical here; the data-modifying-CTE-at-top-level
24588 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24589 /// maps correctly.
24590 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24591 let inner = self.parse_with_cte_then_select()?;
24592 match inner {
24593 Statement::Select(s) => Ok(s),
24594 other => Err(self.err(format!(
24595 "expected SELECT after WITH in a subquery, got {other:?}"
24596 ))),
24597 }
24598 }
24599
24600 /// True when the next token is the (unquoted) WITH keyword. WITH is
24601 /// reserved in PG, so a bare `with` can never be a column reference
24602 /// in these positions; a quoted `"with"` stays an identifier.
24603 fn peek_is_with_kw(&self) -> bool {
24604 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24605 }
24606
24607 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24608 /// `#[inline(never)]` keeps the large SelectStatement temporaries
24609 /// off parse_expr's recursive frame (the nesting-budget stack
24610 /// cliff — see the round-153 gate regression).
24611 #[inline(never)]
24612 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24613 if self.peek_is_with_kw() {
24614 self.advance();
24615 self.parse_nested_with_select()
24616 } else {
24617 match self.parse_select_stmt()? {
24618 Statement::Select(s) => Ok(s),
24619 other => Err(self.err(alloc::format!(
24620 "expected SELECT inside ANY/ALL, got {other:?}"
24621 ))),
24622 }
24623 }
24624 }
24625
24626 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24627 if !matches!(self.peek(), Token::LParen) {
24628 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24629 }
24630 self.advance();
24631 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24632 let s = if self.peek_is_with_kw() {
24633 self.advance();
24634 self.parse_nested_with_select()?
24635 } else {
24636 let inner = self.parse_select_stmt()?;
24637 let Statement::Select(s) = inner else {
24638 unreachable!("parse_select_stmt returns Select")
24639 };
24640 s
24641 };
24642 if !matches!(self.peek(), Token::RParen) {
24643 return Err(self.err(format!(
24644 "expected ')' after EXISTS-subquery, got {:?}",
24645 self.peek()
24646 )));
24647 }
24648 self.advance();
24649 Ok(Expr::Exists {
24650 subquery: Box::new(s),
24651 negated,
24652 })
24653 }
24654
24655 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24656 self.advance(); // IN
24657 if !matches!(self.peek(), Token::LParen) {
24658 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24659 }
24660 self.advance();
24661 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24662 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24663 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24664 let s = if self.peek_is_with_kw() {
24665 self.advance();
24666 self.parse_nested_with_select()?
24667 } else {
24668 let inner = self.parse_select_stmt()?;
24669 let Statement::Select(s) = inner else {
24670 unreachable!("parse_select_stmt always returns Statement::Select")
24671 };
24672 s
24673 };
24674 if !matches!(self.peek(), Token::RParen) {
24675 return Err(self.err(format!(
24676 "expected ')' after IN-subquery, got {:?}",
24677 self.peek()
24678 )));
24679 }
24680 self.advance();
24681 return Ok(Expr::InSubquery {
24682 expr: Box::new(expr),
24683 subquery: Box::new(s),
24684 negated,
24685 });
24686 }
24687 let mut elements = Vec::new();
24688 if !matches!(self.peek(), Token::RParen) {
24689 loop {
24690 elements.push(self.parse_expr(0)?);
24691 match self.peek() {
24692 Token::Comma => {
24693 self.advance();
24694 }
24695 Token::RParen => break,
24696 other => {
24697 return Err(
24698 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24699 );
24700 }
24701 }
24702 }
24703 }
24704 self.advance(); // ')'
24705 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24706 // left-deep OR-Eq chain: chain depth scaled with the element
24707 // count and overflowed the stack (eval + drop are recursive).
24708 if elements.is_empty() {
24709 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24710 }
24711 Ok(Expr::InList {
24712 expr: Box::new(expr),
24713 list: elements,
24714 negated,
24715 })
24716 }
24717
24718 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24719 /// already consumed by the caller. Elements must be numeric literals
24720 /// (with optional unary `-`); any compound expression is rejected at
24721 /// parse time so the runtime never needs to evaluate inside a vector.
24722 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24723 /// has already consumed the `EXTRACT` token before calling us —
24724 /// we pick up at the opening `(`.
24725 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24726 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24727 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24728 /// per-column OR-fold of
24729 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24730 /// term)` so the existing FTS evaluator handles semantics.
24731 ///
24732 /// The mode modifier is accepted-and-ignored at v7.17 — all
24733 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24734 /// mode operators (`+foo -bar`) would need their own parser
24735 /// (Phase 2.2c); customers who hit them today already get a
24736 /// correct lexeme-match against the bare term, only without
24737 /// the +/- precedence the customer asked for.
24738 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24739 // Already at `MATCH`-consumed position; the dispatcher
24740 // confirmed the next token is `(`.
24741 if !matches!(self.peek(), Token::LParen) {
24742 return Err(self.err(alloc::format!(
24743 "expected '(' after MATCH, got {:?}",
24744 self.peek()
24745 )));
24746 }
24747 self.advance();
24748 let mut cols: Vec<Expr> = Vec::new();
24749 loop {
24750 cols.push(self.parse_expr(0)?);
24751 match self.peek() {
24752 Token::Comma => {
24753 self.advance();
24754 }
24755 Token::RParen => break,
24756 other => {
24757 return Err(self.err(alloc::format!(
24758 "expected ',' or ')' in MATCH column list, got {other:?}"
24759 )));
24760 }
24761 }
24762 }
24763 self.advance(); // ')'
24764 // Expect AGAINST.
24765 match self.peek() {
24766 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24767 self.advance();
24768 }
24769 other => {
24770 return Err(self.err(alloc::format!(
24771 "expected AGAINST after MATCH column list, got {other:?}"
24772 )));
24773 }
24774 }
24775 if !matches!(self.peek(), Token::LParen) {
24776 return Err(self.err(alloc::format!(
24777 "expected '(' after AGAINST, got {:?}",
24778 self.peek()
24779 )));
24780 }
24781 self.advance();
24782 // Read AGAINST's argument as a single primary token —
24783 // string literal, placeholder, or column-ref ident. We
24784 // can't call `parse_expr` / `parse_unary` here because
24785 // the postfix chain inside `parse_atom` would greedily
24786 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24787 // and fail at "expected '(' after IN". Customers always
24788 // write a literal or bound parameter in AGAINST, so this
24789 // restriction is non-blocking; the error path explains
24790 // the limit if a more complex expression shows up.
24791 let term = match self.advance() {
24792 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24793 Token::Placeholder(n) => Expr::Placeholder(n),
24794 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24795 qualifier: None,
24796 name: s,
24797 }),
24798 other => {
24799 return Err(self.err(alloc::format!(
24800 "MATCH ... AGAINST(<term>) expects a string literal, \
24801 bound parameter, or column ref, got {other:?}"
24802 )));
24803 }
24804 };
24805 // Optional mode tail — accept-and-ignore at v7.17:
24806 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24807 // IN BOOLEAN MODE
24808 // WITH QUERY EXPANSION
24809 loop {
24810 match self.peek() {
24811 // IN lexes as a reserved Token::In, not an ident,
24812 // so it gets its own arm.
24813 Token::In => {
24814 self.advance();
24815 }
24816 Token::Ident(s) | Token::QuotedIdent(s)
24817 if s.eq_ignore_ascii_case("natural")
24818 || s.eq_ignore_ascii_case("language")
24819 || s.eq_ignore_ascii_case("boolean")
24820 || s.eq_ignore_ascii_case("mode")
24821 || s.eq_ignore_ascii_case("with")
24822 || s.eq_ignore_ascii_case("query")
24823 || s.eq_ignore_ascii_case("expansion") =>
24824 {
24825 self.advance();
24826 }
24827 _ => break,
24828 }
24829 }
24830 if !matches!(self.peek(), Token::RParen) {
24831 return Err(self.err(alloc::format!(
24832 "expected ')' to close AGAINST, got {:?}",
24833 self.peek()
24834 )));
24835 }
24836 self.advance();
24837 // Build per-column `to_tsvector('simple', col) @@
24838 // plainto_tsquery('simple', term)` and OR-fold.
24839 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24840 let plainto = Expr::FunctionCall {
24841 name: String::from("plainto_tsquery"),
24842 args: alloc::vec![simple_lit(), term.clone()],
24843 };
24844 let mut folded: Option<Expr> = None;
24845 for col in cols {
24846 let to_tsv = Expr::FunctionCall {
24847 name: String::from("to_tsvector"),
24848 args: alloc::vec![simple_lit(), col],
24849 };
24850 let leaf = Expr::Binary {
24851 lhs: Box::new(to_tsv),
24852 op: crate::ast::BinOp::TsMatch,
24853 rhs: Box::new(plainto.clone()),
24854 };
24855 folded = Some(match folded {
24856 None => leaf,
24857 Some(prev) => Expr::Binary {
24858 lhs: Box::new(prev),
24859 op: crate::ast::BinOp::Or,
24860 rhs: Box::new(leaf),
24861 },
24862 });
24863 }
24864 match folded {
24865 Some(e) => Ok(e),
24866 None => Err(self.err(String::from(
24867 "MATCH(...) AGAINST(...) requires at least one column",
24868 ))),
24869 }
24870 }
24871
24872 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24873 if !matches!(self.peek(), Token::LParen) {
24874 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24875 }
24876 self.advance();
24877 let field_name = self.expect_ident_like()?;
24878 let field = match field_name.to_ascii_lowercase().as_str() {
24879 // PG accepts the plural spellings (years/months/…/millenniums) as
24880 // aliases for the singular fields — its datetime unit table has both.
24881 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24882 "year" | "years" => ExtractField::Year,
24883 "month" | "months" => ExtractField::Month,
24884 "day" | "days" => ExtractField::Day,
24885 "hour" | "hours" => ExtractField::Hour,
24886 "minute" | "minutes" => ExtractField::Minute,
24887 "second" | "seconds" => ExtractField::Second,
24888 "microsecond" | "microseconds" => ExtractField::Microsecond,
24889 "epoch" => ExtractField::Epoch,
24890 "dow" => ExtractField::Dow,
24891 "isodow" => ExtractField::Isodow,
24892 "doy" => ExtractField::Doy,
24893 "week" | "weeks" => ExtractField::Week,
24894 "isoyear" => ExtractField::Isoyear,
24895 "quarter" => ExtractField::Quarter,
24896 "decade" | "decades" => ExtractField::Decade,
24897 "century" | "centuries" => ExtractField::Century,
24898 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24899 "julian" => ExtractField::Julian,
24900 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24901 "timezone" => ExtractField::Timezone,
24902 "timezone_hour" => ExtractField::TimezoneHour,
24903 "timezone_minute" => ExtractField::TimezoneMinute,
24904 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24905 // reports an unknown one with the source type (22023); carry the
24906 // raw name so eval can word it.
24907 other => ExtractField::Other(alloc::string::String::from(other)),
24908 };
24909 if !matches!(self.peek(), Token::From) {
24910 return Err(self.err(format!(
24911 "expected FROM after EXTRACT field, got {:?}",
24912 self.peek()
24913 )));
24914 }
24915 self.advance();
24916 let source = self.parse_expr(0)?;
24917 if !matches!(self.peek(), Token::RParen) {
24918 return Err(self.err(format!(
24919 "expected ')' to close EXTRACT, got {:?}",
24920 self.peek()
24921 )));
24922 }
24923 self.advance();
24924 Ok(Expr::Extract {
24925 field,
24926 source: Box::new(source),
24927 })
24928 }
24929
24930 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24931 /// is already consumed; we expect a single string literal next and
24932 /// resolve it into `Literal::Interval` at parse time so the engine
24933 /// never has to re-tokenise inside the string.
24934 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24935 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24936 /// is the SQL-standard form and is left to the path below.
24937 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24938 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24939 let (offset, sign) = match self.peek() {
24940 Token::Minus => (1, "-"),
24941 _ => (0, ""),
24942 };
24943 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24944 return None;
24945 };
24946 self.tokens
24947 .get(self.pos + offset + 1)
24948 .filter(|t| mysql_interval_unit(t).is_some())?;
24949 Some((alloc::format!("{sign}{n}"), offset + 1))
24950 }
24951
24952 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24953 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24954 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24955 ///
24956 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24957 /// this by parsing the group and then restoring `self.pos` — which could
24958 /// never have worked, because `advance()` DESTROYS the token it returns
24959 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24960 /// inert only because both branches errored back then.
24961 fn interval_paren_is_quantity(&self) -> bool {
24962 let mut depth = 0usize;
24963 let mut saw_top_level_comma = false;
24964 let mut i = self.pos;
24965 while let Some(tok) = self.tokens.get(i) {
24966 match tok {
24967 Token::LParen => depth += 1,
24968 Token::RParen => {
24969 depth = depth.saturating_sub(1);
24970 if depth == 0 {
24971 return !saw_top_level_comma
24972 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24973 .is_some();
24974 }
24975 }
24976 // A comma directly inside the outermost parens means the
24977 // argument list of the INTERVAL() function.
24978 Token::Comma if depth == 1 => saw_top_level_comma = true,
24979 Token::Eof => return false,
24980 _ => {}
24981 }
24982 i += 1;
24983 }
24984 false
24985 }
24986
24987 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24988 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24989 // (the index of the last Ni ≤ N), distinct from the interval literal.
24990 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24991 // is decided by a non-destructive lookahead (round 422) before either
24992 // branch consumes anything. MySQL only.
24993 if self.mysql_dialect
24994 && matches!(self.peek(), Token::LParen)
24995 && !self.interval_paren_is_quantity()
24996 {
24997 self.advance(); // (
24998 let mut args = Vec::new();
24999 if !matches!(self.peek(), Token::RParen) {
25000 loop {
25001 args.push(self.parse_expr(0)?);
25002 if matches!(self.peek(), Token::Comma) {
25003 self.advance();
25004 continue;
25005 }
25006 break;
25007 }
25008 }
25009 if !matches!(self.peek(), Token::RParen) {
25010 return Err(self.err(alloc::format!(
25011 "expected ')' after INTERVAL() arguments, got {:?}",
25012 self.peek()
25013 )));
25014 }
25015 self.advance(); // )
25016 return Ok(Expr::FunctionCall {
25017 name: alloc::string::String::from("interval"),
25018 args,
25019 });
25020 }
25021 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25022 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25023 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25024 // writes every date arithmetic there is, and it did not parse at
25025 // all. PG rejects the unquoted form outright (`syntax error at or
25026 // near "1"`, measured), so it is taken only in the MySQL dialect —
25027 // PG's own `INTERVAL '1' DAY` is untouched below.
25028 if self.mysql_dialect
25029 && let Some((text, consume)) = self.peek_unquoted_interval_count()
25030 {
25031 for _ in 0..consume {
25032 self.advance(); // the optional `-` and the number
25033 }
25034 let Some(unit) = mysql_interval_unit(self.peek()) else {
25035 return Err(self.err(alloc::format!(
25036 "expected an interval unit after INTERVAL {text}, got {:?}",
25037 self.peek()
25038 )));
25039 };
25040 self.advance(); // the unit
25041 let (months, days, micros) = scale_mysql_interval(&text, unit)
25042 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25043 return Ok(Expr::Literal(Literal::Interval {
25044 months,
25045 days,
25046 micros,
25047 // The canonical rendering, so Display round-trips into a
25048 // form both dialects read back.
25049 text: alloc::format!("{text} {unit}"),
25050 }));
25051 }
25052 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25053 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25054 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25055 // Those cannot fold into a compile-time `Literal::Interval`, so they
25056 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25057 // builtin, which builds the value at run time (and yields NULL for a
25058 // NULL quantity, as MariaDB does). The literal path above still folds
25059 // the constant case — it is cheaper and round-trips through Display.
25060 //
25061 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25062 // MySQL's quoted spelling) keep the qualifier path below.
25063 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25064 let qty = self.parse_expr(0)?;
25065 let Some(unit) = mysql_interval_unit(self.peek()) else {
25066 return Err(self.err(alloc::format!(
25067 "expected an interval unit after INTERVAL <expr>, got {:?}",
25068 self.peek()
25069 )));
25070 };
25071 self.advance(); // the unit
25072 return Ok(make_interval_call(qty, unit));
25073 }
25074 let tok = self.advance();
25075 let Token::String(text) = tok else {
25076 return Err(self.err(format!(
25077 "expected string literal after INTERVAL, got {tok:?}"
25078 )));
25079 };
25080 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25081 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25082 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25083 // bare number means and the leading/trailing precision.
25084 let field1 = interval_field_of(self.peek());
25085 let qualifier = if let Some(f1) = field1 {
25086 self.advance();
25087 let f2 = if matches!(self.peek(), Token::To) {
25088 self.advance();
25089 let Some(f) = interval_field_of(self.peek()) else {
25090 return Err(self.err(format!(
25091 "expected an interval field after TO, got {:?}",
25092 self.peek()
25093 )));
25094 };
25095 self.advance();
25096 Some(f)
25097 } else {
25098 None
25099 };
25100 Some((f1, f2))
25101 } else {
25102 None
25103 };
25104 let (months, days, micros) = match qualifier {
25105 Some(q) => interpret_qualified_interval(&text, q),
25106 None => parse_interval_text(&text),
25107 }
25108 .ok_or_else(|| ParseError {
25109 message: format!(
25110 "cannot parse INTERVAL {text:?}; \
25111 expected `<n> <unit> [<n> <unit> ...]` with units \
25112 microsecond[s], millisecond[s], second[s], minute[s], \
25113 hour[s], day[s], week[s], month[s], year[s]"
25114 ),
25115 token_pos: self.consumed_pos(),
25116 })?;
25117 Ok(Expr::Literal(Literal::Interval {
25118 months,
25119 days,
25120 micros,
25121 text,
25122 }))
25123 }
25124
25125 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25126 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25127 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25128 /// than a pgvector literal.
25129 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25130 self.advance(); // consume `[`
25131 let mut items: Vec<Expr> = Vec::new();
25132 if !matches!(self.peek(), Token::RBracket) {
25133 loop {
25134 if matches!(self.peek(), Token::LBracket) {
25135 items.push(self.parse_array_bracket_body()?);
25136 } else {
25137 items.push(self.parse_expr(0)?);
25138 }
25139 match self.peek() {
25140 Token::Comma => {
25141 self.advance();
25142 }
25143 Token::RBracket => break,
25144 other => {
25145 return Err(self.err(alloc::format!(
25146 "expected ',' or ']' in array literal, got {other:?}"
25147 )));
25148 }
25149 }
25150 }
25151 }
25152 self.advance(); // consume `]`
25153 Ok(Expr::Array(items))
25154 }
25155
25156 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25157 let mut elems = Vec::new();
25158 if matches!(self.peek(), Token::RBracket) {
25159 self.advance();
25160 return Ok(Expr::Literal(Literal::Vector(elems)));
25161 }
25162 loop {
25163 let e = self.parse_expr(0)?;
25164 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25165 message: format!("vector element must be a numeric literal, got {e:?}"),
25166 token_pos: self.pos,
25167 })?;
25168 elems.push(x);
25169 match self.peek() {
25170 Token::Comma => {
25171 self.advance();
25172 }
25173 Token::RBracket => {
25174 self.advance();
25175 break;
25176 }
25177 other => {
25178 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25179 }
25180 }
25181 }
25182 Ok(Expr::Literal(Literal::Vector(elems)))
25183 }
25184
25185 /// Atom that started with an identifier: could be `t.col`, `col`, or
25186 /// `func(arg, ...)`. Detect each shape by looking at the next token.
25187 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25188 /// [, ...])`. Caller has already consumed `OVER`. Either clause
25189 /// is optional; an empty `()` is also legal (PG semantics).
25190 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25191 /// modifier between `name(args)` and `OVER (...)`. Default is
25192 /// `Respect`. Unrecognised idents leave the stream unchanged.
25193 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25194 let Token::Ident(s) = self.peek().clone() else {
25195 return NullTreatment::Respect;
25196 };
25197 let is_ignore = s.eq_ignore_ascii_case("ignore");
25198 let is_respect = s.eq_ignore_ascii_case("respect");
25199 if !is_ignore && !is_respect {
25200 return NullTreatment::Respect;
25201 }
25202 // Lookahead for NULLS — only consume both tokens together.
25203 // pos+1 must hold a "nulls" ident.
25204 if self.pos + 1 < self.tokens.len()
25205 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25206 && s2.eq_ignore_ascii_case("nulls")
25207 {
25208 self.advance();
25209 self.advance();
25210 return if is_ignore {
25211 NullTreatment::Ignore
25212 } else {
25213 NullTreatment::Respect
25214 };
25215 }
25216 NullTreatment::Respect
25217 }
25218
25219 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25220 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25221 /// (same shape as the `OVER` tail). Consumes the whole clause and
25222 /// returns the predicate; returns `None` when no `FILTER` follows.
25223 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25224 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25225 return Ok(None);
25226 };
25227 if !s.eq_ignore_ascii_case("filter") {
25228 return Ok(None);
25229 }
25230 self.advance(); // FILTER
25231 if !matches!(self.peek(), Token::LParen) {
25232 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25233 }
25234 self.advance(); // (
25235 if !matches!(self.peek(), Token::Where) {
25236 return Err(self.err(format!(
25237 "expected WHERE inside FILTER (...), got {:?}",
25238 self.peek()
25239 )));
25240 }
25241 self.advance(); // WHERE
25242 let cond = self.parse_expr(0)?;
25243 if !matches!(self.peek(), Token::RParen) {
25244 return Err(self.err(format!(
25245 "expected ')' to close FILTER (WHERE ...), got {:?}",
25246 self.peek()
25247 )));
25248 }
25249 self.advance(); // )
25250 Ok(Some(Box::new(cond)))
25251 }
25252
25253 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25254 /// the separator as the aggregate's second argument, which is the
25255 /// shape `string_agg` already takes. Returns whether one was there.
25256 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25257 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25258 return Ok(false);
25259 }
25260 self.advance();
25261 let Token::String(sep) = self.peek().clone() else {
25262 return Err(self.err(alloc::format!(
25263 "expected a string literal after SEPARATOR, got {:?}",
25264 self.peek()
25265 )));
25266 };
25267 self.advance();
25268 args.push(Expr::Literal(Literal::String(sep)));
25269 Ok(true)
25270 }
25271
25272 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25273 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25274 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25275 /// keys, or an empty vec when no `WITHIN GROUP` follows.
25276 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25277 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25278 return Ok(Vec::new());
25279 };
25280 if !s.eq_ignore_ascii_case("within") {
25281 return Ok(Vec::new());
25282 }
25283 self.advance(); // WITHIN
25284 if !matches!(self.peek(), Token::Group) {
25285 return Err(self.err(format!(
25286 "expected GROUP after WITHIN, got {:?}",
25287 self.peek()
25288 )));
25289 }
25290 self.advance(); // GROUP
25291 if !matches!(self.peek(), Token::LParen) {
25292 return Err(self.err(format!(
25293 "expected '(' after WITHIN GROUP, got {:?}",
25294 self.peek()
25295 )));
25296 }
25297 self.advance(); // (
25298 if !matches!(self.peek(), Token::Order) {
25299 return Err(self.err(format!(
25300 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25301 self.peek()
25302 )));
25303 }
25304 self.advance(); // ORDER
25305 if !self.peek_is_by() {
25306 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25307 }
25308 self.advance(); // BY
25309 let mut keys: Vec<OrderBy> = Vec::new();
25310 loop {
25311 // v7.39 (round 691) — save/restore, the discipline this parser
25312 // already uses around `pending_sample_preds`, so a subquery inside
25313 // a key neither inherits nor leaks the channel.
25314 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25315 let saved_coll = self.order_key_collation.take();
25316 let parsed = self.parse_expr(0);
25317 self.in_order_by_key = saved_flag;
25318 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25319 let expr = parsed?;
25320 let desc = if matches!(self.peek(), Token::Desc) {
25321 self.advance();
25322 true
25323 } else if matches!(self.peek(), Token::Asc) {
25324 self.advance();
25325 false
25326 } else {
25327 false
25328 };
25329 let nulls_first = self.parse_optional_nulls_placement()?;
25330 keys.push(OrderBy {
25331 expr,
25332 desc,
25333 nulls_first,
25334 collation,
25335 });
25336 if matches!(self.peek(), Token::Comma) {
25337 self.advance();
25338 } else {
25339 break;
25340 }
25341 }
25342 if !matches!(self.peek(), Token::RParen) {
25343 return Err(self.err(format!(
25344 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25345 self.peek()
25346 )));
25347 }
25348 self.advance(); // )
25349 Ok(keys)
25350 }
25351
25352 /// No frame clause is supported.
25353 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25354 fn parse_over_clause(
25355 &mut self,
25356 ) -> Result<
25357 (
25358 Vec<Expr>,
25359 Vec<(Expr, bool, Option<bool>)>,
25360 Option<WindowFrame>,
25361 ),
25362 ParseError,
25363 > {
25364 // `OVER w` — a named-window reference. The WINDOW clause
25365 // parses after the select list, so the name rides out as a
25366 // marker in partition_by; parse_bare_select substitutes the
25367 // definition once the clause is known.
25368 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25369 let name = w.clone();
25370 self.advance();
25371 return Ok((
25372 alloc::vec![Expr::Column(crate::ast::ColumnName {
25373 qualifier: Some("__named_window__".to_string()),
25374 name,
25375 })],
25376 Vec::new(),
25377 None,
25378 ));
25379 }
25380 if !matches!(self.peek(), Token::LParen) {
25381 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25382 }
25383 self.advance();
25384 let mut partition_by = Vec::new();
25385 let mut order_by = Vec::new();
25386 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25387 // window, refined in place. PG's rules (probed against 18.4) differ
25388 // from the bare `OVER w1` form, so the reference rides out under its
25389 // own marker and `substitute_named_windows` applies them. The base
25390 // name is any leading identifier that isn't a window-spec keyword.
25391 let base_window = match self.peek() {
25392 Token::Ident(s) | Token::QuotedIdent(s)
25393 if !s.eq_ignore_ascii_case("partition")
25394 && !s.eq_ignore_ascii_case("rows")
25395 && !s.eq_ignore_ascii_case("range")
25396 && !s.eq_ignore_ascii_case("groups") =>
25397 {
25398 let n = s.clone();
25399 self.advance();
25400 Some(n)
25401 }
25402 _ => None,
25403 };
25404 // PARTITION BY ?
25405 // v7.37.6-B promoted PARTITION to a reserved keyword
25406 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25407 // `Token::Ident("partition")`. Accept both so older sources
25408 // and the new lexer surface land on the same path.
25409 let is_partition_kw = match self.peek() {
25410 Token::Partition => true,
25411 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25412 _ => false,
25413 };
25414 if is_partition_kw {
25415 self.advance();
25416 if !self.peek_is_by() {
25417 return Err(self.err(format!(
25418 "expected BY after PARTITION, got {:?}",
25419 self.peek()
25420 )));
25421 }
25422 self.advance();
25423 loop {
25424 partition_by.push(self.parse_expr(0)?);
25425 if matches!(self.peek(), Token::Comma) {
25426 self.advance();
25427 continue;
25428 }
25429 break;
25430 }
25431 }
25432 // ORDER BY ?
25433 if matches!(self.peek(), Token::Order) {
25434 self.advance();
25435 if !self.peek_is_by() {
25436 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25437 }
25438 self.advance();
25439 loop {
25440 let e = self.parse_expr(0)?;
25441 let desc = if matches!(self.peek(), Token::Desc) {
25442 self.advance();
25443 true
25444 } else if matches!(self.peek(), Token::Asc) {
25445 self.advance();
25446 false
25447 } else {
25448 false
25449 };
25450 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25451 let nulls_first = self.parse_optional_nulls_placement()?;
25452 order_by.push((e, desc, nulls_first));
25453 if matches!(self.peek(), Token::Comma) {
25454 self.advance();
25455 continue;
25456 }
25457 break;
25458 }
25459 }
25460 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25461 // Both keywords come through the lexer as identifiers; match
25462 // case-insensitively.
25463 let mut frame: Option<WindowFrame> = None;
25464 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25465 let kind = if s.eq_ignore_ascii_case("rows") {
25466 Some(FrameKind::Rows)
25467 } else if s.eq_ignore_ascii_case("range") {
25468 Some(FrameKind::Range)
25469 } else if s.eq_ignore_ascii_case("groups") {
25470 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25471 Some(FrameKind::Groups)
25472 } else {
25473 None
25474 };
25475 if let Some(kind) = kind {
25476 self.advance();
25477 frame = Some(self.parse_frame_tail(kind)?);
25478 }
25479 }
25480 if !matches!(self.peek(), Token::RParen) {
25481 return Err(self.err(format!(
25482 "expected ')' to close OVER clause, got {:?}",
25483 self.peek()
25484 )));
25485 }
25486 self.advance();
25487 if let Some(base) = base_window {
25488 // A copy may refine but never override the base's partitioning
25489 // (PG rejects it outright, before looking the name up).
25490 if !partition_by.is_empty() {
25491 return Err(self.err(alloc::format!(
25492 "cannot override PARTITION BY clause of window \"{base}\""
25493 )));
25494 }
25495 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25496 qualifier: Some("__named_window_ref__".to_string()),
25497 name: base,
25498 })];
25499 }
25500 Ok((partition_by, order_by, frame))
25501 }
25502
25503 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25504 /// or `RANGE` keyword was just consumed. Accepts both
25505 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25506 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25507 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25508 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25509 let (start, end) = if matches!(self.peek(), Token::Between) {
25510 self.advance();
25511 let start = self.parse_frame_bound()?;
25512 if !matches!(self.peek(), Token::And) {
25513 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25514 }
25515 self.advance();
25516 let end = self.parse_frame_bound()?;
25517 (start, Some(end))
25518 } else {
25519 (self.parse_frame_bound()?, None)
25520 };
25521 let exclude = self.parse_frame_exclusion()?;
25522 Ok(WindowFrame {
25523 kind,
25524 start,
25525 end,
25526 exclude,
25527 })
25528 }
25529
25530 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25531 /// after a frame spec. NO OTHERS is the default no-op.
25532 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25533 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25534 return Ok(FrameExclusion::NoOthers);
25535 }
25536 self.advance(); // EXCLUDE
25537 match self.peek() {
25538 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25539 self.advance();
25540 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25541 return Err(self.err(format!(
25542 "expected ROW after EXCLUDE CURRENT, got {:?}",
25543 self.peek()
25544 )));
25545 }
25546 self.advance();
25547 Ok(FrameExclusion::CurrentRow)
25548 }
25549 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25550 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25551 // Without this arm it fell to the catch-all, whose message
25552 // self-contradictingly listed GROUP as expected.
25553 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25554 self.advance();
25555 Ok(FrameExclusion::Group)
25556 }
25557 Token::Group => {
25558 self.advance();
25559 Ok(FrameExclusion::Group)
25560 }
25561 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25562 self.advance();
25563 Ok(FrameExclusion::Ties)
25564 }
25565 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25566 self.advance();
25567 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25568 return Err(self.err(format!(
25569 "expected OTHERS after EXCLUDE NO, got {:?}",
25570 self.peek()
25571 )));
25572 }
25573 self.advance();
25574 Ok(FrameExclusion::NoOthers)
25575 }
25576 other => Err(self.err(format!(
25577 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25578 ))),
25579 }
25580 }
25581
25582 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25583 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25584 /// `UNBOUNDED FOLLOWING`.
25585 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25586 // Interval-typed offset for a value-based RANGE frame over a
25587 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25588 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25589 // PRECEDING`.
25590 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25591 let dir = self.expect_ident_like()?;
25592 return if dir.eq_ignore_ascii_case("preceding") {
25593 Ok(FrameBound::IntervalPreceding {
25594 months,
25595 days,
25596 micros,
25597 })
25598 } else if dir.eq_ignore_ascii_case("following") {
25599 Ok(FrameBound::IntervalFollowing {
25600 months,
25601 days,
25602 micros,
25603 })
25604 } else {
25605 Err(self.err(format!(
25606 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25607 )))
25608 };
25609 }
25610 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25611 if let Token::Integer(n) = *self.peek() {
25612 self.advance();
25613 let n: u64 = u64::try_from(n).map_err(|_| {
25614 self.err(format!(
25615 "invalid frame offset {n} — expected non-negative integer"
25616 ))
25617 })?;
25618 let dir = self.expect_ident_like()?;
25619 return if dir.eq_ignore_ascii_case("preceding") {
25620 Ok(FrameBound::OffsetPreceding(n))
25621 } else if dir.eq_ignore_ascii_case("following") {
25622 Ok(FrameBound::OffsetFollowing(n))
25623 } else {
25624 Err(self.err(format!(
25625 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25626 )))
25627 };
25628 }
25629 let first = self.expect_ident_like()?;
25630 if first.eq_ignore_ascii_case("unbounded") {
25631 let dir = self.expect_ident_like()?;
25632 return if dir.eq_ignore_ascii_case("preceding") {
25633 Ok(FrameBound::UnboundedPreceding)
25634 } else if dir.eq_ignore_ascii_case("following") {
25635 Ok(FrameBound::UnboundedFollowing)
25636 } else {
25637 Err(self.err(format!(
25638 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25639 )))
25640 };
25641 }
25642 if first.eq_ignore_ascii_case("current") {
25643 let row = self.expect_ident_like()?;
25644 if !row.eq_ignore_ascii_case("row") {
25645 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25646 }
25647 return Ok(FrameBound::CurrentRow);
25648 }
25649 Err(self.err(format!(
25650 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25651 )))
25652 }
25653
25654 /// Detect and consume a leading interval offset in a frame bound —
25655 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25656 /// `(months, days, micros)`. Leaves the cursor on the trailing
25657 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25658 /// when the next tokens are not an interval offset.
25659 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25660 // Shape A — `INTERVAL '1 day'`.
25661 if matches!(self.peek(), Token::Interval) {
25662 self.advance(); // INTERVAL
25663 let atom = self.parse_interval_atom()?;
25664 if let Expr::Literal(Literal::Interval {
25665 months,
25666 days,
25667 micros,
25668 ..
25669 }) = atom
25670 {
25671 return Ok(Some((months, days, micros)));
25672 }
25673 return Err(self.err("expected an interval literal in frame offset".to_string()));
25674 }
25675 // Shape B — `'1 day'::interval`. Look ahead for the exact
25676 // string / `::` / interval-target triple before committing.
25677 if let Token::String(text) = self.peek() {
25678 let target_is_interval = match self.tokens.get(self.pos + 2) {
25679 Some(Token::Interval) => true,
25680 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25681 _ => false,
25682 };
25683 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25684 && target_is_interval;
25685 if is_cast {
25686 let text = text.clone();
25687 self.advance(); // string
25688 self.advance(); // ::
25689 self.advance(); // interval
25690 let parts = parse_interval_text(&text).ok_or_else(|| {
25691 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25692 })?;
25693 return Ok(Some(parts));
25694 }
25695 }
25696 Ok(None)
25697 }
25698
25699 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25700 // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
25701 // `_binary'z'`. All three were `ERROR 1064 syntax error` here
25702 // and all three answer the literal on MySQL 9.7.2.
25703 //
25704 // It is not only syntax, which is why it waited for
25705 // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
25706 // because `_binary` makes the comparison byte-wise, while
25707 // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
25708 // dropping the charset would have turned a hard error into a
25709 // silently wrong comparison — worse than the error it replaced.
25710 //
25711 // An UNKNOWN charset is NOT an introducer: MySQL answers
25712 // `Unknown column '_nosuch'`, because it parses as a column
25713 // reference followed by a string. So the table decides, and it
25714 // is the same table `SET NAMES` reads.
25715 //
25716 // A space is allowed between the two (`_utf8mb4 'x'`), which
25717 // falls out of asking the token stream rather than the bytes.
25718 if self.mysql_dialect
25719 && let Token::String(_) = self.peek()
25720 {
25721 let lower = first.to_ascii_lowercase();
25722 let charset = if lower == "n" {
25723 // `N'…'` is the national character set, which MySQL
25724 // documents as utf8 — utf8mb3 in 9.7.2's spelling.
25725 //
25726 // utf8mb3 and utf8mb4 both fold case in their default
25727 // collations, so nothing SPG can be asked distinguishes
25728 // the two here: an ablation swapping this to utf8mb4
25729 // reddens no pin. Recorded rather than implied — the
25730 // spelling follows MySQL's documentation, not a
25731 // measurement.
25732 Some("utf8mb3")
25733 } else {
25734 // No filter here: the lookup below IS the test for
25735 // "is this a charset". An ablation that removed a filter
25736 // in this spot reddened nothing, which is how the two
25737 // were found to be one check written twice.
25738 lower.strip_prefix('_')
25739 };
25740 if let Some(cs) = charset
25741 && let Some(collation) = crate::charset::charset_default_collation(cs)
25742 {
25743 let Token::String(body) = self.advance() else {
25744 unreachable!("peeked a string");
25745 };
25746 return Ok(Expr::Collate {
25747 expr: Box::new(Expr::Literal(Literal::String(body))),
25748 collation: String::from(collation),
25749 });
25750 }
25751 }
25752 if matches!(self.peek(), Token::Dot) {
25753 self.advance();
25754 let name = self.expect_ident_like()?;
25755 // v7.14.0 — schema-qualified function call
25756 // `<schema>.<fn>(args)`. PG dumps emit
25757 // `pg_catalog.set_config(...)` in the preamble. SPG
25758 // is single-namespace: drop the schema prefix and
25759 // route the dispatch on the bare function name.
25760 if matches!(self.peek(), Token::LParen) {
25761 return self.finish_ident_atom(name);
25762 }
25763 return Ok(Expr::Column(ColumnName {
25764 qualifier: Some(first),
25765 name,
25766 }));
25767 }
25768 if matches!(self.peek(), Token::LParen) {
25769 self.advance();
25770 // `COUNT(*)` — special-cased here because `*` isn't a normal
25771 // expression token. Lower-case match on `first` since the lexer
25772 // folds identifiers.
25773 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25774 self.advance();
25775 if !matches!(self.peek(), Token::RParen) {
25776 return Err(self.err(format!(
25777 "expected ')' after COUNT(*), got {:?}",
25778 self.peek()
25779 )));
25780 }
25781 self.advance();
25782 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25783 let filter = self.parse_filter_clause()?;
25784 // v4.12: COUNT(*) OVER (...) — same window tail.
25785 let null_treatment = self.parse_null_treatment_modifier();
25786 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25787 && s.eq_ignore_ascii_case("over")
25788 {
25789 self.advance();
25790 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25791 return Ok(Expr::WindowFunction {
25792 name: "count_star".into(),
25793 args: Vec::new(),
25794 partition_by,
25795 order_by,
25796 frame,
25797 null_treatment,
25798 filter,
25799 });
25800 }
25801 if let Some(filter) = filter {
25802 return Ok(Expr::AggregateOrdered {
25803 call: Box::new(Expr::FunctionCall {
25804 name: "count_star".into(),
25805 args: Vec::new(),
25806 }),
25807 order_by: Vec::new(),
25808 distinct: false,
25809 filter: Some(filter),
25810 });
25811 }
25812 return Ok(Expr::FunctionCall {
25813 name: "count_star".into(),
25814 args: Vec::new(),
25815 });
25816 }
25817 // Function call. PG-style: zero-or-more comma-separated args.
25818 let mut args = Vec::new();
25819 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25820 // Names are collected in lock-step with `args` and resolved to
25821 // positional order after the loop (the AST stays positional).
25822 let mut arg_names: Vec<Option<String>> = Vec::new();
25823 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25824 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25825 // seen, so the value arguments before it can be folded.
25826 let mut saw_separator = false;
25827 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25828 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25829 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25830 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25831 self.advance();
25832 true
25833 } else if matches!(self.peek(), Token::All) {
25834 self.advance();
25835 false
25836 } else {
25837 false
25838 };
25839 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25840 // TIMESTAMPDIFF take a bare unit keyword as the first
25841 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25842 // bare type keyword (DATE / TIME / DATETIME); lower them
25843 // onto string literals so the evaluator sees plain text.
25844 if ((first.eq_ignore_ascii_case("timestampadd")
25845 || first.eq_ignore_ascii_case("timestampdiff"))
25846 && matches!(self.peek(), Token::Ident(u) if matches!(
25847 u.to_ascii_lowercase().as_str(),
25848 "microsecond" | "second" | "minute" | "hour" | "day"
25849 | "week" | "month" | "quarter" | "year"
25850 )))
25851 || (first.eq_ignore_ascii_case("get_format")
25852 && matches!(self.peek(), Token::Ident(u) if matches!(
25853 u.to_ascii_lowercase().as_str(),
25854 "date" | "time" | "datetime" | "timestamp"
25855 )))
25856 {
25857 if let Token::Ident(u) = self.peek() {
25858 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25859 }
25860 self.advance();
25861 if matches!(self.peek(), Token::Comma) {
25862 self.advance();
25863 }
25864 }
25865 // `ROW(a, b, …)` keyword constructor. Followed by a
25866 // comparison operator or [NOT] IN it joins the paren
25867 // row-constructor machinery (fieldwise parse-time
25868 // expansion); bare, it stays a `row` call the evaluator
25869 // renders as PG record text.
25870 if first.eq_ignore_ascii_case("row") {
25871 let mut row_items = Vec::new();
25872 if !matches!(self.peek(), Token::RParen) {
25873 loop {
25874 row_items.push(self.parse_expr(0)?);
25875 match self.peek() {
25876 Token::Comma => {
25877 self.advance();
25878 }
25879 Token::RParen => break,
25880 other => {
25881 return Err(self.err(format!(
25882 "expected ',' or ')' in ROW(...), got {other:?}"
25883 )));
25884 }
25885 }
25886 }
25887 }
25888 self.advance(); // ')'
25889 let comparison_follows = matches!(
25890 self.peek(),
25891 Token::Eq
25892 | Token::NotEq
25893 | Token::Lt
25894 | Token::LtEq
25895 | Token::Gt
25896 | Token::GtEq
25897 | Token::In
25898 ) || (matches!(self.peek(), Token::Not)
25899 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25900 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25901 if comparison_follows && !row_items.is_empty() {
25902 return self.parse_row_comparison_tail(row_items);
25903 }
25904 return Ok(Expr::FunctionCall {
25905 name: String::from("row"),
25906 args: row_items,
25907 });
25908 }
25909 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25910 // the parse-mode keyword introduces the source text. SPG
25911 // carries XML as text, so both modes lower to __xmlparse(expr)
25912 // which validates well-formedness and returns Value::Xml.
25913 if first.eq_ignore_ascii_case("xmlparse")
25914 && matches!(self.peek(), Token::Ident(kw)
25915 if kw.eq_ignore_ascii_case("document")
25916 || kw.eq_ignore_ascii_case("content"))
25917 {
25918 let mode = match self.advance() {
25919 Token::Ident(kw) => kw.to_ascii_lowercase(),
25920 _ => unreachable!("peeked an ident"),
25921 };
25922 let src = self.parse_expr(0)?;
25923 if !matches!(self.peek(), Token::RParen) {
25924 return Err(self.err(format!(
25925 "expected ')' to close XMLPARSE, got {:?}",
25926 self.peek()
25927 )));
25928 }
25929 self.advance();
25930 return Ok(Expr::FunctionCall {
25931 name: String::from("__xmlparse"),
25932 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25933 });
25934 }
25935 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25936 // keyword introduces the element name (a bare or quoted
25937 // identifier), then optional content expressions. Lower to a
25938 // plain `xmlelement(name_text, content …)` call.
25939 if first.eq_ignore_ascii_case("xmlelement")
25940 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25941 {
25942 self.advance(); // consume NAME
25943 let elem_name = match self.peek().clone() {
25944 Token::Ident(n) | Token::QuotedIdent(n) => {
25945 self.advance();
25946 n
25947 }
25948 other => {
25949 return Err(self.err(format!(
25950 "expected element name after XMLELEMENT NAME, got {other:?}"
25951 )));
25952 }
25953 };
25954 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25955 while matches!(self.peek(), Token::Comma) {
25956 self.advance();
25957 args.push(self.parse_expr(0)?);
25958 }
25959 if !matches!(self.peek(), Token::RParen) {
25960 return Err(self.err(format!(
25961 "expected ')' to close XMLELEMENT, got {:?}",
25962 self.peek()
25963 )));
25964 }
25965 self.advance();
25966 return Ok(Expr::FunctionCall {
25967 name: String::from("xmlelement"),
25968 args,
25969 });
25970 }
25971 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25972 // becomes a `<name>value</name>` element; a bare column infers its
25973 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25974 // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
25975 // `CONVERT(expr USING cs)` was a syntax error at USING, and
25976 // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
25977 // `convert(bytea, src, dest)` and answered `column "char" does
25978 // not exist`. Both are casts in MySQL: measured on 9.7.2,
25979 // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
25980 // both 'A', and `CONVERT(123, CHAR)` is '123'.
25981 //
25982 // The charset is checked against the same table the introducers
25983 // use, so an unknown one is refused rather than quietly ignored.
25984 if self.mysql_dialect
25985 && first.eq_ignore_ascii_case("convert")
25986 && !matches!(self.peek(), Token::RParen)
25987 {
25988 let save = self.pos;
25989 let inner = self.parse_expr(0)?;
25990 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
25991 self.advance();
25992 let cs = match self.peek().clone() {
25993 Token::Ident(n) | Token::QuotedIdent(n) => {
25994 self.advance();
25995 n
25996 }
25997 other => {
25998 return Err(self.err(alloc::format!(
25999 "expected a charset after USING, got {other:?}"
26000 )));
26001 }
26002 };
26003 let lc = cs.to_ascii_lowercase();
26004 if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26005 return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26006 }
26007 if !matches!(self.peek(), Token::RParen) {
26008 return Err(self.err(alloc::format!(
26009 "expected ')' after CONVERT … USING, got {:?}",
26010 self.peek()
26011 )));
26012 }
26013 self.advance();
26014 let target = if lc == "binary" {
26015 CastTarget::Named("binary".to_string())
26016 } else {
26017 CastTarget::Text
26018 };
26019 return self.finish_postfix_casts(Expr::Cast {
26020 expr: alloc::boxed::Box::new(inner),
26021 target,
26022 });
26023 }
26024 if matches!(self.peek(), Token::Comma) {
26025 self.advance();
26026 // A type name here is MySQL's cast form; anything else
26027 // (three string arguments) is PostgreSQL's `convert`,
26028 // which keeps its own path.
26029 if let Ok(target) = self.parse_cast_target()
26030 && matches!(self.peek(), Token::RParen)
26031 {
26032 self.advance();
26033 return self.finish_postfix_casts(Expr::Cast {
26034 expr: alloc::boxed::Box::new(inner),
26035 target,
26036 });
26037 }
26038 }
26039 self.pos = save;
26040 }
26041 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26042 let mut args: Vec<Expr> = Vec::new();
26043 loop {
26044 let val = self.parse_expr(0)?;
26045 let name = if matches!(self.peek(), Token::As) {
26046 self.advance();
26047 match self.peek().clone() {
26048 Token::Ident(n) | Token::QuotedIdent(n) => {
26049 self.advance();
26050 n
26051 }
26052 other => {
26053 return Err(self.err(format!(
26054 "expected name after AS in XMLFOREST, got {other:?}"
26055 )));
26056 }
26057 }
26058 } else if let Expr::Column(c) = &val {
26059 c.name.clone()
26060 } else {
26061 return Err(
26062 self.err("XMLFOREST element without a column name needs AS".into())
26063 );
26064 };
26065 args.push(Expr::Literal(Literal::String(name)));
26066 args.push(val);
26067 if matches!(self.peek(), Token::Comma) {
26068 self.advance();
26069 } else {
26070 break;
26071 }
26072 }
26073 if !matches!(self.peek(), Token::RParen) {
26074 return Err(self.err(format!(
26075 "expected ')' to close XMLFOREST, got {:?}",
26076 self.peek()
26077 )));
26078 }
26079 self.advance();
26080 return Ok(Expr::FunctionCall {
26081 name: String::from("xmlforest"),
26082 args,
26083 });
26084 }
26085 // SQL-standard `POSITION(sub IN str)` — lowers onto
26086 // strpos(str, sub). IN is the argument separator here,
26087 // so the needle parses with the IN-tail suppressed.
26088 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26089 let saved = self.suppress_in_tail;
26090 self.suppress_in_tail = true;
26091 let needle = self.parse_expr(0);
26092 self.suppress_in_tail = saved;
26093 let needle = needle?;
26094 if matches!(self.peek(), Token::In) {
26095 self.advance();
26096 let haystack = self.parse_expr(0)?;
26097 if !matches!(self.peek(), Token::RParen) {
26098 return Err(self.err(format!(
26099 "expected ')' to close POSITION, got {:?}",
26100 self.peek()
26101 )));
26102 }
26103 self.advance();
26104 return Ok(Expr::FunctionCall {
26105 name: String::from("strpos"),
26106 args: alloc::vec![haystack, needle],
26107 });
26108 }
26109 // position(sub, str) comma form (incl. bytea) —
26110 // hand the parsed first arg to the generic list.
26111 args.push(needle);
26112 if matches!(self.peek(), Token::Comma) {
26113 self.advance();
26114 }
26115 }
26116 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26117 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26118 // plain comma forms TRIM(str) / TRIM(str, chars) keep
26119 // riding the generic argument list below.
26120 if first.eq_ignore_ascii_case("trim") {
26121 let mode = match self.peek() {
26122 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26123 self.advance();
26124 Some("btrim")
26125 }
26126 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26127 self.advance();
26128 Some("ltrim")
26129 }
26130 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26131 self.advance();
26132 Some("rtrim")
26133 }
26134 _ => None,
26135 };
26136 if mode.is_some() || matches!(self.peek(), Token::From) {
26137 // TRIM([mode] FROM str) — no strip-chars.
26138 let chars = if matches!(self.peek(), Token::From) {
26139 None
26140 } else {
26141 Some(self.parse_expr(0)?)
26142 };
26143 if !matches!(self.peek(), Token::From) {
26144 return Err(self.err(format!(
26145 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26146 self.peek()
26147 )));
26148 }
26149 self.advance();
26150 let target = self.parse_expr(0)?;
26151 if !matches!(self.peek(), Token::RParen) {
26152 return Err(
26153 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26154 );
26155 }
26156 self.advance();
26157 let mut trim_args = alloc::vec![target];
26158 if let Some(c) = chars {
26159 trim_args.push(c);
26160 }
26161 return Ok(Expr::FunctionCall {
26162 name: String::from(mode.unwrap_or("btrim")),
26163 args: trim_args,
26164 });
26165 }
26166 }
26167 if !matches!(self.peek(), Token::RParen) {
26168 loop {
26169 // v7.38 (read01, T14) — `argname => value` names this arg.
26170 // v7.39 (read01 round 77) — `argname := value` is the same
26171 // thing, and it is the spelling PG's own docs lead with. It
26172 // was simply never lexed here, so every `f(x := 1)` died in
26173 // the parser regardless of what `f` was.
26174 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26175 (
26176 Token::Ident(n) | Token::QuotedIdent(n),
26177 Some(Token::FatArrow | Token::ColonEq),
26178 ) => {
26179 let name = n.clone();
26180 self.advance(); // name
26181 self.advance(); // => / :=
26182 Some(name)
26183 }
26184 _ => None,
26185 };
26186 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26187 // array's elements into a variadic call's trailing args
26188 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26189 // reserved, so it arrives as a bare ident before the arg.
26190 let is_variadic = this_name.is_none()
26191 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26192 if is_variadic {
26193 self.advance();
26194 }
26195 let arg = self.parse_expr(0)?;
26196 args.push(match &this_name {
26197 // The callee's parameter names decide the slot, and a
26198 // user function's live in the catalog. Carry the name
26199 // to eval rather than guessing here.
26200 Some(n) => Expr::NamedArg {
26201 name: n.clone(),
26202 expr: Box::new(arg),
26203 },
26204 None if is_variadic => Expr::Variadic(Box::new(arg)),
26205 None => arg,
26206 });
26207 arg_names.push(this_name);
26208 // v7.25 (round-17) — standard `CAST(expr AS type)`.
26209 // The `::` cast already worked; this lowers the
26210 // function form onto the same Expr::Cast node.
26211 if first.eq_ignore_ascii_case("cast")
26212 && args.len() == 1
26213 && matches!(self.peek(), Token::As)
26214 {
26215 self.advance();
26216 let target = self.parse_cast_target()?;
26217 if !matches!(self.peek(), Token::RParen) {
26218 return Err(self.err(format!(
26219 "expected ')' to close CAST, got {:?}",
26220 self.peek()
26221 )));
26222 }
26223 self.advance();
26224 return Ok(Expr::Cast {
26225 expr: Box::new(args.pop().expect("one arg")),
26226 target,
26227 });
26228 }
26229 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26230 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26231 // keywords; SPG's lexer makes them plain idents (so they'd be
26232 // read as column refs). Lower the keyword to the string form
26233 // the evaluator already accepts.
26234 if first.eq_ignore_ascii_case("normalize")
26235 && args.len() == 1
26236 && matches!(self.peek(), Token::Comma)
26237 {
26238 let form = match self.tokens.get(self.pos + 1) {
26239 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26240 let up = f.to_ascii_uppercase();
26241 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26242 }
26243 _ => None,
26244 };
26245 if let Some(up) = form {
26246 self.advance(); // comma
26247 self.advance(); // form keyword
26248 args.push(Expr::Literal(Literal::String(up)));
26249 }
26250 }
26251 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26252 // form. Desugars to the comma-list shape evaluator already
26253 // handles. Triggered after the first arg when the function
26254 // name is substring / substr and the next token is FROM
26255 // (a reserved keyword in PG; SPG also reserves it).
26256 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26257 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26258 // internal __substring_similar(str, pat, esc) call.
26259 if (first.eq_ignore_ascii_case("substring")
26260 || first.eq_ignore_ascii_case("substr"))
26261 && args.len() == 1
26262 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26263 {
26264 self.advance(); // SIMILAR
26265 let pattern = self.parse_expr(0)?;
26266 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26267 {
26268 return Err(self.err(format!(
26269 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26270 self.peek()
26271 )));
26272 }
26273 self.advance(); // ESCAPE
26274 let esc = self.parse_expr(0)?;
26275 if !matches!(self.peek(), Token::RParen) {
26276 return Err(self.err(format!(
26277 "expected ')' to close substring(... SIMILAR ...), got {:?}",
26278 self.peek()
26279 )));
26280 }
26281 self.advance();
26282 args.push(pattern);
26283 args.push(esc);
26284 return Ok(Expr::FunctionCall {
26285 name: "__substring_similar".to_string(),
26286 args,
26287 });
26288 }
26289 if (first.eq_ignore_ascii_case("substring")
26290 || first.eq_ignore_ascii_case("substr"))
26291 && args.len() == 1
26292 && matches!(self.peek(), Token::From | Token::For)
26293 {
26294 // `substring(str FROM pos [FOR len])`, or the FOR-only
26295 // `substring(str FOR len)` which PG treats as FROM 1.
26296 if matches!(self.peek(), Token::From) {
26297 self.advance();
26298 let start = self.parse_expr(0)?;
26299 args.push(start);
26300 } else {
26301 args.push(Expr::Literal(Literal::Integer(1)));
26302 }
26303 if matches!(self.peek(), Token::For) {
26304 self.advance();
26305 let length = self.parse_expr(0)?;
26306 args.push(length);
26307 }
26308 if !matches!(self.peek(), Token::RParen) {
26309 return Err(self.err(format!(
26310 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26311 self.peek()
26312 )));
26313 }
26314 self.advance();
26315 return Ok(Expr::FunctionCall {
26316 name: first.to_ascii_lowercase(),
26317 args,
26318 });
26319 }
26320 // PG `overlay(str PLACING repl FROM n [FOR len])`
26321 // syntactic form. Desugars to the `overlay(str,
26322 // repl, n[, len])` comma-list shape the evaluator
26323 // already implements. `PLACING` is not a reserved
26324 // token in SPG, so it arrives as a bare Ident.
26325 if first.eq_ignore_ascii_case("overlay")
26326 && args.len() == 1
26327 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26328 {
26329 self.advance(); // consume PLACING
26330 args.push(self.parse_expr(0)?); // replacement
26331 if !matches!(self.peek(), Token::From) {
26332 return Err(self.err(format!(
26333 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26334 self.peek()
26335 )));
26336 }
26337 self.advance();
26338 args.push(self.parse_expr(0)?); // start position
26339 if matches!(self.peek(), Token::For) {
26340 self.advance();
26341 args.push(self.parse_expr(0)?); // length
26342 }
26343 if !matches!(self.peek(), Token::RParen) {
26344 return Err(self.err(format!(
26345 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26346 self.peek()
26347 )));
26348 }
26349 self.advance();
26350 return Ok(Expr::FunctionCall {
26351 name: String::from("overlay"),
26352 args,
26353 });
26354 }
26355 // `TRIM(chars FROM str)` — the keyword-less
26356 // spelling lands here after the chars parse
26357 // (the keyword forms return earlier).
26358 if first.eq_ignore_ascii_case("trim")
26359 && args.len() == 1
26360 && matches!(self.peek(), Token::From)
26361 {
26362 self.advance();
26363 let target = self.parse_expr(0)?;
26364 if !matches!(self.peek(), Token::RParen) {
26365 return Err(self.err(format!(
26366 "expected ')' to close TRIM(chars FROM str), got {:?}",
26367 self.peek()
26368 )));
26369 }
26370 self.advance();
26371 let chars = args.pop().expect("one arg");
26372 return Ok(Expr::FunctionCall {
26373 name: String::from("btrim"),
26374 args: alloc::vec![target, chars],
26375 });
26376 }
26377 // v7.24 (round-16 A) — aggregate-internal
26378 // ordering: `array_agg(x ORDER BY y DESC NULLS
26379 // LAST)`. Keys close the argument list.
26380 if matches!(self.peek(), Token::Order) {
26381 self.advance();
26382 if !self.peek_is_by() {
26383 return Err(self.err(format!(
26384 "expected BY after ORDER in aggregate args, got {:?}",
26385 self.peek()
26386 )));
26387 }
26388 self.advance();
26389 loop {
26390 // v7.39 (round 691) — save/restore, the discipline this parser
26391 // already uses around `pending_sample_preds`, so a subquery inside
26392 // a key neither inherits nor leaks the channel.
26393 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26394 let saved_coll = self.order_key_collation.take();
26395 let parsed = self.parse_expr(0);
26396 self.in_order_by_key = saved_flag;
26397 let collation =
26398 core::mem::replace(&mut self.order_key_collation, saved_coll);
26399 let expr = parsed?;
26400 let desc = if matches!(self.peek(), Token::Desc) {
26401 self.advance();
26402 true
26403 } else if matches!(self.peek(), Token::Asc) {
26404 self.advance();
26405 false
26406 } else {
26407 false
26408 };
26409 let nulls_first = self.parse_optional_nulls_placement()?;
26410 agg_order_by.push(OrderBy {
26411 expr,
26412 desc,
26413 nulls_first,
26414 collation,
26415 });
26416 if matches!(self.peek(), Token::Comma) {
26417 self.advance();
26418 } else {
26419 break;
26420 }
26421 }
26422 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26423 // follow the ORDER BY inside GROUP_CONCAT.
26424 if self.consume_group_concat_separator(&mut args)? {
26425 saw_separator = true;
26426 }
26427 if !matches!(self.peek(), Token::RParen) {
26428 return Err(self.err(format!(
26429 "expected ')' after aggregate ORDER BY, got {:?}",
26430 self.peek()
26431 )));
26432 }
26433 break;
26434 }
26435 // v7.39 (round 354, M12) — …or directly after the
26436 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26437 // own spelling of what PG passes as string_agg's second
26438 // argument; it was a parse error, so every MySQL query
26439 // that names its own separator failed outright.
26440 if self.consume_group_concat_separator(&mut args)? {
26441 saw_separator = true;
26442 break;
26443 }
26444 match self.peek() {
26445 Token::Comma => {
26446 self.advance();
26447 }
26448 Token::RParen => break,
26449 other => {
26450 return Err(self.err(format!(
26451 "expected ',' or ')' in function args, got {other:?}"
26452 )));
26453 }
26454 }
26455 }
26456 }
26457 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26458 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26459 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26460 // meaning a separator — that is what the explicit SEPARATOR
26461 // tail is for. Fold them into one `concat(...)` so the
26462 // aggregate keeps its single value argument.
26463 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26464 let values = args.len() - usize::from(saw_separator);
26465 if values > 1 {
26466 let sep_arg = if saw_separator { args.pop() } else { None };
26467 let folded = Expr::FunctionCall {
26468 name: "concat".to_string(),
26469 args: core::mem::take(&mut args),
26470 };
26471 args.push(folded);
26472 if let Some(sep) = sep_arg {
26473 args.push(sep);
26474 }
26475 }
26476 }
26477 self.advance(); // consume ')'
26478 // v7.39 (read01 round 77) — named arguments are NOT reordered here
26479 // any more. The parser has no catalog, so it could only ever resolve
26480 // the handful of `make_*` builtins whose parameter names were baked
26481 // into a table right here — every user function got
26482 // "does not support named arguments", though the catalog has been
26483 // storing its parameter names all along. Reordering happens in eval,
26484 // in one place, for builtins and user functions alike.
26485 // v7.32 (round-29) — ordered-set aggregate tail
26486 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26487 // (percentile_cont / percentile_disc / mode). The sort spec
26488 // lands in the same `order_by` slot a decorated aggregate
26489 // uses; the executor dispatches on the function name. WITHIN
26490 // GROUP and an intra-argument ORDER BY are mutually
26491 // exclusive (PG rejects both).
26492 let within_group_order = self.parse_within_group_clause()?;
26493 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26494 return Err(self.err(
26495 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26496 .into(),
26497 ));
26498 }
26499 let within_group_seen = !within_group_order.is_empty();
26500 let agg_order_by = if within_group_order.is_empty() {
26501 agg_order_by
26502 } else {
26503 within_group_order
26504 };
26505 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26506 let filter = self.parse_filter_clause()?;
26507 // v4.12: window-function tail — `name(args) OVER (...)`.
26508 // Promotes the just-parsed FunctionCall into a
26509 // WindowFunction node carrying partition + order.
26510 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26511 // / `RESPECT NULLS OVER (...)` between the closing paren
26512 // and `OVER`.
26513 let null_treatment = self.parse_null_treatment_modifier();
26514 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26515 && s.eq_ignore_ascii_case("over")
26516 {
26517 self.advance();
26518 // v7.39 (round 230) — PG implements neither modifier for a
26519 // windowed call and says so (0A000). Both used to be parsed
26520 // and then silently dropped here, so `count(DISTINCT v)
26521 // OVER (…)` quietly answered the non-distinct count.
26522 if agg_distinct {
26523 return Err(
26524 self.err("DISTINCT is not implemented for window functions".to_string())
26525 );
26526 }
26527 if !agg_order_by.is_empty() {
26528 // PG separates the two shapes that land here: a
26529 // WITHIN GROUP call is an ordered-set aggregate and gets
26530 // its own message naming the aggregate; a plain
26531 // `agg(x ORDER BY y)` gets the generic one.
26532 let msg = if within_group_seen {
26533 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26534 } else {
26535 "aggregate ORDER BY is not implemented for window functions".to_string()
26536 };
26537 return Err(self.err(msg));
26538 }
26539 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26540 return Ok(Expr::WindowFunction {
26541 name: first,
26542 args,
26543 partition_by,
26544 order_by,
26545 frame,
26546 null_treatment,
26547 filter,
26548 });
26549 }
26550 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26551 return Ok(Expr::AggregateOrdered {
26552 call: Box::new(Expr::FunctionCall { name: first, args }),
26553 order_by: agg_order_by,
26554 distinct: agg_distinct,
26555 filter,
26556 });
26557 }
26558 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26559 // over TIMESTAMPTZ and has no timestamp overload, so a
26560 // timestamp argument is coerced on the way in and the answer
26561 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26562 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26563 // zone`. SPG answered `timestamp without time zone`, dropping
26564 // the offset from every rendering.
26565 //
26566 // Writing the coercion PG performs makes the existing
26567 // argument-driven typing (the one `date_trunc` uses) reach the
26568 // right answer, rather than teaching the type layer a second
26569 // rule. MySQL's DATE_ADD is a different function that returns
26570 // DATE or DATETIME, so this is PG-dialect only.
26571 //
26572 // Out-of-line because this sits on the RECURSIVE descent
26573 // frame: an inline block with locals here costs every nesting
26574 // level, and the suite's deep-nesting sentinel overflowed the
26575 // 512 KiB parser stack the moment one was added (round 430's
26576 // lesson, in the same shape).
26577 if !self.mysql_dialect {
26578 lift_date_add_arg_to_timestamptz(&first, &mut args);
26579 }
26580 return Ok(Expr::FunctionCall { name: first, args });
26581 }
26582 // v7.9.20 — SQL-standard parenless keyword expressions
26583 // (PG treats these as functions called without parens).
26584 // Resolve to a synthetic FunctionCall so the engine's
26585 // eval path reuses the existing function-call routing.
26586 // mailrs G3.
26587 let lc = first.to_ascii_lowercase();
26588 if matches!(
26589 lc.as_str(),
26590 "current_date"
26591 | "current_time"
26592 | "current_timestamp"
26593 | "localtimestamp"
26594 | "localtime"
26595 // v7.37.17 (17.6 siblings) — session-identity SQL-
26596 // standard parenless keywords. current_user /
26597 // session_user / user were already caught by the
26598 // pgwire canned-response shortcut but bare-select
26599 // in the embedded engine went through Expr::Column
26600 // and errored. Adding them here so the parser
26601 // resolves to a synthetic FunctionCall that reuses
26602 // the existing eval/functions.rs dispatch.
26603 | "current_user"
26604 | "session_user"
26605 | "current_role"
26606 | "current_catalog"
26607 | "current_schema"
26608 | "current_database"
26609 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26610 | "system_user"
26611 ) {
26612 return Ok(Expr::FunctionCall {
26613 name: lc,
26614 args: Vec::new(),
26615 });
26616 }
26617 Ok(Expr::Column(ColumnName {
26618 qualifier: None,
26619 name: first,
26620 }))
26621 }
26622}
26623
26624/// v7.39 (round 522) — write the coercion PG's `date_add` /
26625/// `date_subtract` signature performs.
26626///
26627/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26628/// timestamp argument is cast on the way in and the answer is
26629/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26630/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26631/// `timestamp without time zone`, dropping the offset from every
26632/// rendering of the result.
26633///
26634/// Writing the cast the signature implies lets the existing
26635/// argument-driven typing (the one `date_trunc` uses) reach the right
26636/// answer instead of teaching the type layer a second rule. MySQL's
26637/// DATE_ADD is a different function returning DATE or DATETIME, so the
26638/// caller applies this in PG dialect only.
26639///
26640/// A free function, and not a block at the call site, because the caller
26641/// is on the recursive-descent frame chain.
26642#[inline(never)]
26643fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26644 if args.len() != 2
26645 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26646 {
26647 return;
26648 }
26649 let base = args.remove(0);
26650 args.insert(
26651 0,
26652 Expr::Cast {
26653 expr: Box::new(base),
26654 target: CastTarget::Timestamptz,
26655 },
26656 );
26657}
26658
26659/// v6.8.2 — walk an expression tree and return the first column
26660/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26661/// to derive `CreateIndexStatement.column` from an expression
26662/// key (so downstream planner code resolving a primary column
26663/// position keeps working with expression indexes). Returns
26664/// `None` when the expression has no column ref at all — caller
26665/// surfaces that as a parse error.
26666fn extract_first_column(expr: &Expr) -> Option<String> {
26667 match expr {
26668 Expr::Column(cn) => Some(cn.name.clone()),
26669 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26670 Expr::Binary { lhs, rhs, .. } => {
26671 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26672 }
26673 Expr::Unary { expr: e, .. } => extract_first_column(e),
26674 // v7.39 (read01 round 93) — a cast wraps its operand: a common
26675 // expression-index key is `lower(col::text)`, where the column
26676 // sits under the `::text` cast inside the function arg. Without
26677 // descending here the key was rejected as "references no column".
26678 Expr::Cast { expr: e, .. } => extract_first_column(e),
26679 // v7.39.2 — and a COLLATE wraps its operand the same way.
26680 // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
26681 // column the moment the clause became a node instead of being
26682 // absorbed, and the key was rejected as referencing none. This
26683 // is the shape the wildcard below silently produces, which is
26684 // why it is spelled out.
26685 Expr::Collate { expr: e, .. } => extract_first_column(e),
26686 _ => None,
26687 }
26688}
26689
26690fn maybe_not(expr: Expr, negated: bool) -> Expr {
26691 if negated {
26692 Expr::Unary {
26693 op: UnOp::Not,
26694 expr: Box::new(expr),
26695 }
26696 } else {
26697 expr
26698 }
26699}
26700
26701/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26702/// things in the two dialects, and SPG read all three PG's way:
26703///
26704/// | token | PG (and SPG) | MySQL, measured |
26705/// |---|---|---|
26706/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26707/// | `&&` | inet / array overlap | **AND** |
26708/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26709///
26710/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26711/// answer with no error, which is why they are routed here rather than
26712/// left to the shared table.
26713impl Parser {
26714 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26715 if self.mysql_dialect {
26716 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26717 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26718 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26719 if let Token::Ident(w) = tok
26720 && w.eq_ignore_ascii_case("div")
26721 {
26722 return Some((BinOp::IntDiv, 8));
26723 }
26724 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26725 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26726 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26727 // there sits in operand position, not infix).
26728 if let Token::Ident(w) = tok
26729 && w.eq_ignore_ascii_case("mod")
26730 {
26731 return Some((BinOp::Mod, 8));
26732 }
26733 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26734 // plain ident to the lexer. Its precedence sits between OR (1)
26735 // and AND (3) — hence rung 2, the slot freed by moving AND up.
26736 if let Token::Ident(w) = tok
26737 && w.eq_ignore_ascii_case("xor")
26738 {
26739 return Some((BinOp::LogicalXor, 2));
26740 }
26741 match tok {
26742 Token::Concat => return Some((BinOp::Or, 1)),
26743 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26744 Token::InetOverlap => return Some((BinOp::And, 3)),
26745 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26746 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26747 _ => {}
26748 }
26749 }
26750 binop_from(tok)
26751 }
26752}
26753
26754// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26755// (which sits strictly between OR and AND), every level from AND upward was
26756// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26757// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26758// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26759// the *relative* order of every PG operator is unchanged by the shift.
26760fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26761 let pair = match tok {
26762 Token::Or => (BinOp::Or, 1),
26763 Token::And => (BinOp::And, 3),
26764 Token::Eq => (BinOp::Eq, 5),
26765 Token::NotEq => (BinOp::NotEq, 5),
26766 Token::Lt => (BinOp::Lt, 5),
26767 Token::LtEq => (BinOp::LtEq, 5),
26768 Token::Gt => (BinOp::Gt, 5),
26769 Token::GtEq => (BinOp::GtEq, 5),
26770 // pgvector distance ops all sit on the same rung — tighter than
26771 // comparisons (5) so `col <-> v < threshold` parses correctly.
26772 Token::L2Distance => (BinOp::L2Distance, 6),
26773 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26774 // comparison rung.
26775 Token::GeomParallel => (BinOp::GeomParallel, 5),
26776 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26777 // comparison rung.
26778 Token::OverLeft => (BinOp::OverLeft, 5),
26779 Token::OverRight => (BinOp::OverRight, 5),
26780 Token::GeomPerp => (BinOp::GeomPerp, 5),
26781 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26782 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26783 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26784 Token::InnerProduct => (BinOp::InnerProduct, 6),
26785 Token::CosineDistance => (BinOp::CosineDistance, 6),
26786 Token::Plus => (BinOp::Add, 7),
26787 Token::Minus => (BinOp::Sub, 7),
26788 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26789 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26790 // pgvector distances above) BETWEEN additive (7) and the
26791 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26792 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26793 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26794 // ("matches PG conceptually" — the round-753 audit measured it
26795 // false; the old rung errored on `'a' || 1 + 1` with
26796 // `text + integer`). Same-level chains left-fold, as PG does.
26797 Token::Concat => (BinOp::Concat, 6),
26798 Token::Pipe => (BinOp::BitOr, 6),
26799 Token::Amp => (BinOp::BitAnd, 6),
26800 Token::Star => (BinOp::Mul, 8),
26801 Token::Slash => (BinOp::Div, 8),
26802 Token::Percent => (BinOp::Mod, 8),
26803 // v4.14: JSON path ops bind tighter than comparisons (5)
26804 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26805 // Same rung as the multiplicative ops.
26806 Token::JsonGet => (BinOp::JsonGet, 8),
26807 Token::JsonGetText => (BinOp::JsonGetText, 8),
26808 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26809 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26810 Token::JsonContains => (BinOp::JsonContains, 8),
26811 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26812 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26813 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26814 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26815 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26816 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26817 // v7.12.2 — `@@` binds at the comparison rung (looser than
26818 // arithmetic, tighter than AND / OR). PG places `@@` at
26819 // the same precedence as `=` / `<`, so we follow.
26820 Token::TsMatch => (BinOp::TsMatch, 5),
26821 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26822 // PG places these at the comparison rung (same level as `=`),
26823 // so we follow.
26824 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26825 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26826 Token::InetContains => (BinOp::InetContains, 5),
26827 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26828 Token::InetOverlap => (BinOp::InetOverlap, 5),
26829 // v7.39 (round 508) — the geometric and pattern-order predicates
26830 // ride the comparison rung, as every other predicate does.
26831 Token::Intersects => (BinOp::Intersects, 5),
26832 Token::IsBelow => (BinOp::IsBelow, 5),
26833 Token::IsAbove => (BinOp::IsAbove, 5),
26834 Token::PatternLt => (BinOp::PatternLt, 5),
26835 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26836 Token::PatternGt => (BinOp::PatternGt, 5),
26837 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26838 // `@@@` is the old spelling of `@@` and means exactly it.
26839 Token::TsMatchOld => (BinOp::TsMatch, 5),
26840 _ => return None,
26841 };
26842 Some(pair)
26843}
26844
26845#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26846// `as f32` here is intentional: vector elements widen / narrow into f32 on
26847// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26848// past ~15 decimal digits — both are acceptable for a fixed-precision
26849// pgvector column.
26850/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26851/// implicit table alias and break trailing clauses. WITH lands
26852/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26853/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26854/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26855/// / VALUES / FOR / LATERAL — all of which would otherwise be
26856/// silently swallowed by `parse_optional_alias`.
26857fn is_alias_stopword(s: &str) -> bool {
26858 matches!(
26859 s.to_ascii_lowercase().as_str(),
26860 "with"
26861 | "on"
26862 | "where"
26863 | "having"
26864 | "group"
26865 | "order"
26866 | "limit"
26867 | "offset"
26868 | "union"
26869 | "except"
26870 | "intersect"
26871 | "returning"
26872 | "set"
26873 | "values"
26874 | "for"
26875 | "window"
26876 | "tablesample"
26877 | "lateral"
26878 | "left"
26879 | "right"
26880 | "inner"
26881 | "outer"
26882 | "full"
26883 | "cross"
26884 | "join"
26885 | "natural"
26886 | "using"
26887 | "fetch"
26888 )
26889}
26890
26891fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26892 match e {
26893 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26894 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26895 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26896 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26897 // so scale the divisor by hand instead of `f32::powi`.)
26898 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26899 let mut div = 1.0f32;
26900 for _ in 0..*scale {
26901 div *= 10.0;
26902 }
26903 Some(*unscaled as f32 / div)
26904 }
26905 Expr::Unary {
26906 op: UnOp::Neg,
26907 expr,
26908 } => extract_numeric_literal(expr).map(|x| -x),
26909 _ => None,
26910 }
26911}
26912
26913/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26914/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26915/// negative. Returns `None` if any pair fails to parse or no pair is found.
26916///
26917/// Recognised units (case-insensitive, optional trailing `s`):
26918/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26919/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26920/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26921/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26922/// (PG-canonical: DST and month-boundary semantics depend on this).
26923/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26924/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26925/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26926#[allow(clippy::cast_possible_truncation)]
26927fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26928 let mut months: i64 = 0;
26929 let mut days: i64 = 0;
26930 let mut micros: i64 = 0;
26931 let mut in_time = false;
26932 let mut num = alloc::string::String::new();
26933 for ch in rest.chars() {
26934 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26935 num.push(ch);
26936 continue;
26937 }
26938 if ch == 'T' || ch == 't' {
26939 if !num.is_empty() {
26940 return None;
26941 }
26942 in_time = true;
26943 continue;
26944 }
26945 let n: f64 = num.parse().ok()?;
26946 num.clear();
26947 match (ch, in_time) {
26948 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26949 ('M', false) => months += n as i64,
26950 ('W' | 'w', false) => days += (n * 7.0) as i64,
26951 ('D' | 'd', false) => days += n as i64,
26952 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26953 ('M', true) => micros += (n * 60_000_000.0) as i64,
26954 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26955 _ => return None,
26956 }
26957 }
26958 if !num.is_empty() {
26959 return None;
26960 }
26961 Some((
26962 i32::try_from(months).ok()?,
26963 i32::try_from(days).ok()?,
26964 micros,
26965 ))
26966}
26967
26968/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26969/// leading `-` negates the whole value). Rejects date-like strings.
26970fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26971 let (neg, body) = match s.strip_prefix('-') {
26972 Some(b) => (true, b),
26973 None => (false, s),
26974 };
26975 let (y, m) = body.split_once('-')?;
26976 let years: i32 = y.parse().ok()?;
26977 let mons: i32 = m.parse().ok()?;
26978 if years < 0 || mons < 0 {
26979 return None;
26980 }
26981 let total = years.checked_mul(12)?.checked_add(mons)?;
26982 Some((if neg { -total } else { total }, 0, 0))
26983}
26984
26985/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26986/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26987fn parse_interval_clock(tok: &str) -> Option<i64> {
26988 let (neg, body) = match tok.strip_prefix('-') {
26989 Some(r) => (true, r),
26990 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26991 };
26992 let mut it = body.split(':');
26993 let h: i64 = it.next()?.parse().ok()?;
26994 let m: i64 = it.next()?.parse().ok()?;
26995 let s_tok = it.next().unwrap_or("0");
26996 if it.next().is_some() {
26997 return None;
26998 }
26999 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27000 let sec: i64 = sec.parse().ok()?;
27001 let mut f = alloc::string::String::from(frac);
27002 while f.len() < 6 {
27003 f.push('0');
27004 }
27005 f.truncate(6);
27006 let fus: i64 = f.parse().ok()?;
27007 sec.checked_mul(1_000_000)?.checked_add(fus)?
27008 } else {
27009 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27010 };
27011 let total = h
27012 .checked_mul(3_600_000_000)?
27013 .checked_add(m.checked_mul(60_000_000)?)?
27014 .checked_add(sec_us)?;
27015 Some(if neg { -total } else { total })
27016}
27017
27018/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27019/// every spelling PG accepts (measured against live PG18.4, not guessed):
27020/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27021/// Before this, the unit table matched long names only, with an ad-hoc
27022/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27023/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27024/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27025/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27026/// fractional) both read from this one table now.
27027fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27028 let u = raw.to_ascii_lowercase();
27029 Some(match u.as_str() {
27030 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27031 "microsecond"
27032 }
27033 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27034 "millisecond"
27035 }
27036 "second" | "seconds" | "sec" | "secs" | "s" => "second",
27037 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27038 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27039 "day" | "days" | "d" => "day",
27040 "week" | "weeks" | "w" => "week",
27041 "month" | "months" | "mon" | "mons" => "month",
27042 "year" | "years" | "yr" | "yrs" | "y" => "year",
27043 "decade" | "decades" | "dec" | "decs" => "decade",
27044 "century" | "centuries" | "cent" | "c" => "century",
27045 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27046 _ => return None,
27047 })
27048}
27049
27050/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27051/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27052#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27053pub(crate) enum IntervalField {
27054 Year,
27055 Month,
27056 Day,
27057 Hour,
27058 Minute,
27059 Second,
27060}
27061
27062/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27063/// spellings aren't standard for the qualifier position, so only the singular
27064/// forms are accepted.
27065/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27066/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27067/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27068/// take a `'1 2'` style literal — are not read here; they stay a parse
27069/// error rather than being silently misread.)
27070/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27071///
27072/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27073/// to do with a `@@` engine setting, and an unset one reads NULL rather
27074/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27075/// were the same node and `SELECT @x` answered "Unknown system variable".)
27076/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27077/// not see a session override — measured, after `SET autocommit=0`,
27078/// `@@global.autocommit` is still 1.
27079///
27080/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27081/// the parser's nesting budget is tuned against, and building these
27082/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27083/// wall `parse_left_right_atom` and friends were factored out for).
27084#[inline(never)]
27085fn variable_ref_atom(raw: &str) -> Expr {
27086 let user_var = !raw.starts_with("@@");
27087 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27088 Expr::FunctionCall {
27089 name: String::from(if user_var {
27090 "__spg_user_var"
27091 } else {
27092 "__spg_session_var"
27093 }),
27094 args: alloc::vec![Expr::Literal(Literal::String(bare))],
27095 }
27096}
27097
27098fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27099 let Token::Ident(s) = tok else { return None };
27100 Some(match () {
27101 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27102 () if s.eq_ignore_ascii_case("second") => "second",
27103 () if s.eq_ignore_ascii_case("minute") => "minute",
27104 () if s.eq_ignore_ascii_case("hour") => "hour",
27105 () if s.eq_ignore_ascii_case("day") => "day",
27106 () if s.eq_ignore_ascii_case("week") => "week",
27107 () if s.eq_ignore_ascii_case("month") => "month",
27108 () if s.eq_ignore_ascii_case("quarter") => "quarter",
27109 () if s.eq_ignore_ascii_case("year") => "year",
27110 () => return None,
27111 })
27112}
27113
27114/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27115/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27116/// which constructs the value at run time. Only the slot the unit names
27117/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27118/// slot the builtin has (months and fractional seconds respectively).
27119fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27120 let zero = || Expr::Literal(Literal::Integer(0));
27121 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27122 lhs: alloc::boxed::Box::new(qty.clone()),
27123 op,
27124 rhs: alloc::boxed::Box::new(by),
27125 };
27126 // (years, months, weeks, days, hours, mins, secs)
27127 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27128 match unit {
27129 "year" => args[0] = qty,
27130 "quarter" => {
27131 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27132 }
27133 "month" => args[1] = qty,
27134 "week" => args[2] = qty,
27135 "day" => args[3] = qty,
27136 "hour" => args[4] = qty,
27137 "minute" => args[5] = qty,
27138 "second" => args[6] = qty,
27139 // The builtin's seconds slot takes a fraction, so microseconds ride
27140 // it scaled down; the divisor is a NUMERIC literal so the division
27141 // stays exact rather than going through a float.
27142 "microsecond" => {
27143 args[6] = scaled(
27144 crate::ast::BinOp::Div,
27145 Expr::Literal(Literal::Numeric {
27146 unscaled: 1_000_000,
27147 scale: 0,
27148 }),
27149 );
27150 }
27151 _ => args[3] = qty,
27152 }
27153 Expr::FunctionCall {
27154 name: alloc::string::String::from("make_interval"),
27155 args,
27156 }
27157}
27158
27159/// `(count, unit)` → `(months, days, micros)`.
27160fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27161 let n: i64 = count.trim().parse().ok()?;
27162 Some(match unit {
27163 "microsecond" => (0, 0, n),
27164 "second" => (0, 0, n.checked_mul(1_000_000)?),
27165 "minute" => (0, 0, n.checked_mul(60_000_000)?),
27166 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27167 "day" => (0, i32::try_from(n).ok()?, 0),
27168 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27169 "month" => (i32::try_from(n).ok()?, 0, 0),
27170 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27171 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27172 _ => return None,
27173 })
27174}
27175
27176fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27177 let Token::Ident(s) = tok else { return None };
27178 Some(match () {
27179 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27180 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27181 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27182 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27183 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27184 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27185 () => return None,
27186 })
27187}
27188
27189/// v7.39 (read01 round 102) — interpret an interval literal under a field
27190/// qualifier. Returns `(months, days, micros)`.
27191///
27192/// * A single field applied to a bare number sets which unit the number means,
27193/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27194/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27195/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27196/// * Every other range, and any literal a single field can't read as a plain
27197/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27198/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27199/// like PG, and the qualifier there only bounds precision.
27200fn interpret_qualified_interval(
27201 text: &str,
27202 (f1, f2): (IntervalField, Option<IntervalField>),
27203) -> Option<(i32, i32, i64)> {
27204 if let Some(f2) = f2 {
27205 if f1 == IntervalField::Year && f2 == IntervalField::Month {
27206 if let Some(m) = parse_year_month_literal(text) {
27207 return Some((m, 0, 0));
27208 }
27209 }
27210 return parse_interval_text(text);
27211 }
27212 // Single field: reinterpret a bare number; otherwise the default parse.
27213 let trimmed = text.trim();
27214 if let Ok(val) = trimmed.parse::<f64>() {
27215 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27216 #[allow(clippy::cast_possible_truncation)]
27217 let whole = val as i64;
27218 #[allow(clippy::cast_possible_truncation)]
27219 let secs_micros = {
27220 let m = val * 1_000_000.0;
27221 if m >= 0.0 {
27222 (m + 0.5) as i64
27223 } else {
27224 (m - 0.5) as i64
27225 }
27226 };
27227 return Some(match f1 {
27228 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27229 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27230 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27231 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27232 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27233 IntervalField::Second => (0, 0, secs_micros),
27234 });
27235 }
27236 parse_interval_text(text)
27237}
27238
27239/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27240fn parse_year_month_literal(text: &str) -> Option<i32> {
27241 let t = text.trim();
27242 let (neg, body) = match t.strip_prefix('-') {
27243 Some(r) => (true, r),
27244 None => (false, t.strip_prefix('+').unwrap_or(t)),
27245 };
27246 let mut it = body.split('-');
27247 let years: i32 = it.next()?.trim().parse().ok()?;
27248 let months: i32 = match it.next() {
27249 Some(m) => m.trim().parse().ok()?,
27250 None => 0,
27251 };
27252 if it.next().is_some() {
27253 return None;
27254 }
27255 let total = years.checked_mul(12)?.checked_add(months)?;
27256 Some(if neg { -total } else { total })
27257}
27258
27259pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27260 // v7.38.19 — the two infinities, answered as the three extreme
27261 // fields PostgreSQL itself puts on the wire for them:
27262 //
27263 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27264 // … 7fffffffffffffff 7fffffff 7fffffff
27265 //
27266 // So no caller has to know the spelling — every one of them already
27267 // reads the three numbers, and `IntervalKind::from_fields` names
27268 // what they mean.
27269 //
27270 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27271 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27272 // infinity. Interval takes the full word, in any case.
27273 {
27274 let word = s.trim();
27275 let word = word.strip_prefix('@').map_or(word, str::trim);
27276 let (neg, body) = match word.strip_prefix('-') {
27277 Some(rest) => (true, rest.trim_start()),
27278 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27279 };
27280 if body.eq_ignore_ascii_case("infinity") {
27281 return Some(if neg {
27282 (i32::MIN, i32::MIN, i64::MIN)
27283 } else {
27284 (i32::MAX, i32::MAX, i64::MAX)
27285 });
27286 }
27287 }
27288 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27289 // `@` is decorative; a trailing `ago` negates the whole interval.
27290 let mut trimmed = s.trim();
27291 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27292 let mut negate = false;
27293 if let Some(rest) = trimmed
27294 .strip_suffix("ago")
27295 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27296 {
27297 negate = true;
27298 trimmed = rest.trim();
27299 }
27300 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27301 let (mo, d, us) = v?;
27302 if negate {
27303 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27304 } else {
27305 Some((mo, d, us))
27306 }
27307 };
27308 let s = trimmed;
27309 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27310 // are single tokens, not the `<n> <unit>` pair form handled below.
27311 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27312 return finish(parse_iso8601_interval(rest));
27313 }
27314 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27315 if let Some(iv) = parse_year_month_interval(trimmed) {
27316 return finish(Some(iv));
27317 }
27318 }
27319 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27320 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27321 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27322 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27323 if let Ok(n) = trimmed.parse::<i64>() {
27324 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27325 }
27326 if let Ok(f) = trimmed.parse::<f64>() {
27327 if f.is_finite() {
27328 #[allow(clippy::cast_possible_truncation)]
27329 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27330 }
27331 }
27332 }
27333 // v7.39 (round 243) — PG accepts the number and unit run together
27334 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27335 // the `<n> <unit>` pair loop below sees them as two.
27336 let raw_parts: Vec<&str> = s.split_whitespace().collect();
27337 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27338 for p in raw_parts {
27339 let boundary = p
27340 .char_indices()
27341 .find(|(i, c)| {
27342 *i > 0
27343 && c.is_ascii_alphabetic()
27344 && p[..*i]
27345 .chars()
27346 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27347 && p[..*i].chars().any(|d| d.is_ascii_digit())
27348 })
27349 .map(|(i, _)| i);
27350 match boundary {
27351 Some(i) => {
27352 parts.push(&p[..i]);
27353 parts.push(&p[i..]);
27354 }
27355 None => parts.push(p),
27356 }
27357 }
27358 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27359 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27360 // remains is the `<n> <unit>` pair form handled below.
27361 let mut clock_us: i64 = 0;
27362 let mut had_clock = false;
27363 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27364 clock_us = parse_interval_clock(parts[pos])?;
27365 parts.remove(pos);
27366 had_clock = true;
27367 }
27368 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27369 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27370 let mut lone_days: i32 = 0;
27371 if had_clock && parts.len() == 1 {
27372 if let Ok(n) = parts[0].parse::<i64>() {
27373 lone_days = i32::try_from(n).ok()?;
27374 parts.clear();
27375 }
27376 }
27377 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27378 return None;
27379 }
27380 let mut months: i32 = 0;
27381 let mut days: i32 = lone_days;
27382 let mut micros: i64 = clock_us;
27383 let mut i = 0;
27384 while i < parts.len() {
27385 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27386 if let Ok(n) = parts[i].parse::<i64>() {
27387 match unit_stripped {
27388 "microsecond" => micros = micros.checked_add(n)?,
27389 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27390 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27391 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27392 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27393 "day" => {
27394 let n32 = i32::try_from(n).ok()?;
27395 days = days.checked_add(n32)?;
27396 }
27397 "week" => {
27398 let n32 = i32::try_from(n).ok()?;
27399 days = days.checked_add(n32.checked_mul(7)?)?;
27400 }
27401 "month" => {
27402 let n32 = i32::try_from(n).ok()?;
27403 months = months.checked_add(n32)?;
27404 }
27405 "year" => {
27406 let n32 = i32::try_from(n).ok()?;
27407 months = months.checked_add(n32.checked_mul(12)?)?;
27408 }
27409 // v7.39 (read01 timestamp.c) — the larger calendar units.
27410 "decade" => {
27411 let n32 = i32::try_from(n).ok()?;
27412 months = months.checked_add(n32.checked_mul(120)?)?;
27413 }
27414 "century" => {
27415 let n32 = i32::try_from(n).ok()?;
27416 months = months.checked_add(n32.checked_mul(1200)?)?;
27417 }
27418 "millennium" => {
27419 let n32 = i32::try_from(n).ok()?;
27420 months = months.checked_add(n32.checked_mul(12000)?)?;
27421 }
27422 _ => return None,
27423 }
27424 } else if let Ok(f) = parts[i].parse::<f64>() {
27425 // Fractional units cascade down to the next-finer field the way
27426 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27427 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27428 // no_std: f64 has no trunc/fract/round methods, so do them with
27429 // casts (toward-zero) + explicit round-half-away-from-zero.
27430 #[allow(clippy::cast_possible_truncation)]
27431 fn round_i64(x: f64) -> i64 {
27432 if x >= 0.0 {
27433 (x + 0.5) as i64
27434 } else {
27435 (x - 0.5) as i64
27436 }
27437 }
27438 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27439 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27440 const DAY_US: f64 = 86_400_000_000.0;
27441 let whole = d as i64; // truncates toward zero
27442 let frac = d - whole as f64;
27443 *days = days.checked_add(i32::try_from(whole).ok()?)?;
27444 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27445 Some(())
27446 }
27447 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27448 match unit_stripped {
27449 "microsecond" => micros = micros.checked_add(round_i64(f))?,
27450 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27451 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27452 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27453 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27454 "day" => add_days_frac(&mut days, &mut micros, f)?,
27455 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27456 "month" => {
27457 let whole = f as i64;
27458 months = months.checked_add(i32::try_from(whole).ok()?)?;
27459 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27460 }
27461 "year" => {
27462 let m = f * 12.0;
27463 let whole = m as i64;
27464 months = months.checked_add(i32::try_from(whole).ok()?)?;
27465 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27466 }
27467 _ => return None,
27468 }
27469 } else {
27470 return None;
27471 }
27472 i += 2;
27473 }
27474 finish(Some((months, days, micros)))
27475}
27476
27477/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27478/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27479/// `interval` is intentionally absent (handled by its own parser arm).
27480/// Returns `None` for names that aren't sensible as a bare typed literal, so
27481/// the caller falls back to treating the ident as a column reference.
27482fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27483 Some(match ident {
27484 "date" => CastTarget::Date,
27485 "timestamp" | "datetime" => CastTarget::Timestamp,
27486 "timestamptz" => CastTarget::Timestamptz,
27487 "bool" | "boolean" => CastTarget::Bool,
27488 "int" | "integer" | "int4" => CastTarget::Int,
27489 "bigint" | "int8" => CastTarget::BigInt,
27490 "float8" | "double precision" => CastTarget::Float,
27491 "uuid" => CastTarget::Uuid,
27492 "bytea" => CastTarget::Bytea,
27493 "json" => CastTarget::Json,
27494 "jsonb" => CastTarget::Jsonb,
27495 // Types without a dedicated CastTarget variant flow through the
27496 // generic Named path (engine resolves via column_type_to_data_type).
27497 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27498 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27499 | "money" | "bit" | "varbit"
27500 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27501 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27502 // Range / multirange types likewise.
27503 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27504 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27505 | "datemultirange" | "tsmultirange" | "tstzmultirange"
27506 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27507 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27508 CastTarget::Named(alloc::string::String::from(ident))
27509 }
27510 _ => return None,
27511 })
27512}
27513
27514/// v7.12.4 — map a bare type-name identifier (the form that
27515/// appears in a function arg list or RETURNS clause) to a
27516/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27517/// types so the caller can preserve them as
27518/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27519///
27520/// Subset of the full column-type grammar — we deliberately
27521/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27522/// here because function-arg types in v7.12.4 are mostly the
27523/// bare form (`text`, `int`, `bytea`, …).
27524/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27525/// than being `name TYPE`?
27526///
27527/// The multi-word spellings SQL allows for a bare argument type, each
27528/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27529///
27530/// NOTE this list also exists in `spg-storage`, which computes the
27531/// signature key from the rendered argument text and has to reach the
27532/// same verdict. The two crates are siblings — neither depends on the
27533/// other — and each already carries its own table of type spellings
27534/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27535/// there), so this follows the structure rather than inventing new
27536/// duplication. Recorded as V49.
27537pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27538 let t = phrase.trim().to_ascii_lowercase();
27539 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27540 matches!(
27541 base,
27542 "double precision"
27543 | "character varying"
27544 | "bit varying"
27545 | "timestamp with time zone"
27546 | "timestamp without time zone"
27547 | "time with time zone"
27548 | "time without time zone"
27549 | "national character"
27550 | "national character varying"
27551 )
27552}
27553
27554fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27555 Some(match ident.to_ascii_lowercase().as_str() {
27556 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27557 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27558 "bigint" => ColumnTypeName::BigInt,
27559 "float" | "double" => ColumnTypeName::Float,
27560 // v7.39 (round 269) — real is 32-bit.
27561 "real" | "float4" => ColumnTypeName::Real,
27562 "text" => ColumnTypeName::Text,
27563 "bool" | "boolean" => ColumnTypeName::Bool,
27564 "date" => ColumnTypeName::Date,
27565 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27566 "timestamptz" => ColumnTypeName::Timestamptz,
27567 "json" => ColumnTypeName::Json,
27568 "jsonb" => ColumnTypeName::Jsonb,
27569 "bytea" | "bytes" => ColumnTypeName::Bytes,
27570 "tsvector" => ColumnTypeName::TsVector,
27571 "tsquery" => ColumnTypeName::TsQuery,
27572 "uuid" => ColumnTypeName::Uuid,
27573 "interval" => ColumnTypeName::Interval,
27574 "time" => ColumnTypeName::Time,
27575 "year" => ColumnTypeName::Year,
27576 "timetz" => ColumnTypeName::TimeTz,
27577 "money" => ColumnTypeName::Money,
27578 _ => return None,
27579 })
27580}
27581
27582/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27583/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27584///
27585/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27586/// / embedded SQL land in v7.12.5+):
27587///
27588/// ```text
27589/// body := [ws] block [ws]
27590/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27591/// stmt := assign | return
27592/// assign := assign_target := expr
27593/// assign_target := ( NEW | OLD ) . ident | ident
27594/// return := RETURN ( NEW | OLD | NULL | expr )
27595/// ```
27596///
27597/// `expr` is parsed by recursing into the regular `Parser` — so a
27598/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27599/// NEW.subject || ' ' || NEW.sender)` body shape works without
27600/// the body parser knowing what `to_tsvector` is.
27601///
27602/// Errors here cause the caller to fall back to
27603/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27604/// successful, but the executor will refuse to invoke the
27605/// function with an "unparseable body" error.
27606/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27607/// from the crate root as `spg_sql::parse_function_body`.
27608pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27609 parse_plpgsql_body(body)
27610}
27611
27612fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27613 // Use the regular lexer on the body text. The trailing
27614 // `END;` may or may not have a semicolon; the lexer treats
27615 // both forms identically.
27616 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27617 message: alloc::format!("plpgsql body lex error: {e}"),
27618 token_pos: 0,
27619 })?;
27620 let mut parser = Parser::new(tokens);
27621 parser.parse_plpgsql_block()
27622}
27623
27624/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27625fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27626 match v {
27627 crate::ast::SetValue::String(s)
27628 | crate::ast::SetValue::Ident(s)
27629 | crate::ast::SetValue::Number(s) => s.clone(),
27630 crate::ast::SetValue::Default => "DEFAULT".into(),
27631 }
27632}
27633
27634/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27635/// contains an aggregate call at ITS OWN query level (recursion stops at
27636/// sublink boundaries — a sublink's aggregates belong to the sublink).
27637/// Backs the "aggregate functions are not allowed in a recursive query's
27638/// recursive term" well-formedness check.
27639fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27640 const AGG_NAMES: &[&str] = &[
27641 "count",
27642 "sum",
27643 "min",
27644 "max",
27645 "avg",
27646 "string_agg",
27647 "array_agg",
27648 "bool_and",
27649 "bool_or",
27650 "every",
27651 "any_value",
27652 "json_agg",
27653 "jsonb_agg",
27654 "json_object_agg",
27655 "jsonb_object_agg",
27656 "bit_and",
27657 "bit_or",
27658 "bit_xor",
27659 "var_pop",
27660 "var_samp",
27661 "variance",
27662 "stddev",
27663 "stddev_pop",
27664 "stddev_samp",
27665 "range_agg",
27666 "range_intersect_agg",
27667 "percentile_cont",
27668 "percentile_disc",
27669 "mode",
27670 "corr",
27671 "covar_pop",
27672 "covar_samp",
27673 ];
27674 match e {
27675 Expr::AggregateOrdered { .. } => true,
27676 Expr::FunctionCall { name, args } => {
27677 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27678 || args.iter().any(expr_has_toplevel_aggregate)
27679 }
27680 Expr::NamedArg { expr, .. }
27681 | Expr::Variadic(expr)
27682 | Expr::Unary { expr, .. }
27683 | Expr::Cast { expr, .. }
27684 | Expr::IsNull { expr, .. }
27685 | Expr::FieldAccess { base: expr, .. }
27686 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27687 Expr::Binary { lhs, rhs, .. } => {
27688 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27689 }
27690 Expr::Like { expr, pattern, .. } => {
27691 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27692 }
27693 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27694 Expr::InList { expr, list, .. } => {
27695 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27696 }
27697 Expr::ArraySubscript { target, index } => {
27698 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27699 }
27700 Expr::ArraySlice { target, lo, hi } => {
27701 expr_has_toplevel_aggregate(target)
27702 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27703 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27704 }
27705 Expr::AnyAll { expr, array, .. } => {
27706 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27707 }
27708 Expr::Case {
27709 operand,
27710 branches,
27711 else_branch,
27712 } => {
27713 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27714 || branches
27715 .iter()
27716 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27717 || else_branch
27718 .as_deref()
27719 .is_some_and(expr_has_toplevel_aggregate)
27720 }
27721 // The outer-level operands of a sublink can aggregate; the sublink's
27722 // own body cannot leak its aggregates up here.
27723 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27724 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27725 row.iter().any(expr_has_toplevel_aggregate)
27726 }
27727 _ => false,
27728 }
27729}
27730
27731/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27732/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27733/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27734/// sublink and is legal in a recursive term, so it is not walked here.
27735fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27736 let mut exprs: Vec<&Expr> = Vec::new();
27737 for it in &s.items {
27738 if let crate::ast::SelectItem::Expr { expr, .. } = it {
27739 exprs.push(expr);
27740 }
27741 }
27742 if let Some(w) = &s.where_ {
27743 exprs.push(w);
27744 }
27745 if let Some(h) = &s.having {
27746 exprs.push(h);
27747 }
27748 if let Some(g) = &s.group_by {
27749 exprs.extend(g.iter());
27750 }
27751 if let Some(from) = &s.from {
27752 for j in &from.joins {
27753 if let Some(on) = &j.on {
27754 exprs.push(on);
27755 }
27756 }
27757 }
27758 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27759}
27760
27761/// Does this expression contain a sublink whose subquery mentions `name`?
27762fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27763 match e {
27764 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27765 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27766 Expr::InSubquery { expr, subquery, .. } => {
27767 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27768 }
27769 Expr::RowInSubquery { row, subquery, .. } => {
27770 row.iter().any(|x| expr_sublink_mentions(x, name))
27771 || select_mentions_table(subquery, name)
27772 }
27773 Expr::RowCmpSubquery { row, subquery, .. } => {
27774 row.iter().any(|x| expr_sublink_mentions(x, name))
27775 || select_mentions_table(subquery, name)
27776 }
27777 Expr::NamedArg { expr, .. }
27778 | Expr::Variadic(expr)
27779 | Expr::Unary { expr, .. }
27780 | Expr::Cast { expr, .. }
27781 | Expr::IsNull { expr, .. }
27782 | Expr::FieldAccess { base: expr, .. }
27783 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27784 Expr::Binary { lhs, rhs, .. } => {
27785 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27786 }
27787 Expr::Like { expr, pattern, .. } => {
27788 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27789 }
27790 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27791 args.iter().any(|x| expr_sublink_mentions(x, name))
27792 }
27793 Expr::InList { expr, list, .. } => {
27794 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27795 }
27796 Expr::ArraySubscript { target, index } => {
27797 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27798 }
27799 Expr::ArraySlice { target, lo, hi } => {
27800 expr_sublink_mentions(target, name)
27801 || lo
27802 .as_deref()
27803 .is_some_and(|x| expr_sublink_mentions(x, name))
27804 || hi
27805 .as_deref()
27806 .is_some_and(|x| expr_sublink_mentions(x, name))
27807 }
27808 Expr::AnyAll { expr, array, .. } => {
27809 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27810 }
27811 Expr::Case {
27812 operand,
27813 branches,
27814 else_branch,
27815 } => {
27816 operand
27817 .as_deref()
27818 .is_some_and(|x| expr_sublink_mentions(x, name))
27819 || branches
27820 .iter()
27821 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27822 || else_branch
27823 .as_deref()
27824 .is_some_and(|x| expr_sublink_mentions(x, name))
27825 }
27826 _ => false,
27827 }
27828}
27829
27830/// Does this SELECT (in full — FROM tables, derived tables, its own
27831/// sublinks, and union arms) mention the named table?
27832fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27833 if let Some(from) = &s.from {
27834 if from.primary.name.eq_ignore_ascii_case(name) {
27835 return true;
27836 }
27837 if let Some(sub) = &from.primary.lateral_subquery
27838 && select_mentions_table(sub, name)
27839 {
27840 return true;
27841 }
27842 for j in &from.joins {
27843 if j.table.name.eq_ignore_ascii_case(name) {
27844 return true;
27845 }
27846 if let Some(sub) = &j.table.lateral_subquery
27847 && select_mentions_table(sub, name)
27848 {
27849 return true;
27850 }
27851 }
27852 }
27853 if select_has_self_ref_in_sublink(s, name) {
27854 return true;
27855 }
27856 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27857}
27858
27859/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27860/// row count, the way PG evaluates one before applying it.
27861///
27862/// `None` = not a constant (a column, a subquery, a function call).
27863/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27864/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27865/// All wordings were read off live PG 18.4.
27866fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27867 use crate::ast::{BinOp, Expr, Literal, UnOp};
27868 match e {
27869 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27870 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27871 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27872 }
27873 // PG coerces a string by its CONTENT, and fails on the value.
27874 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27875 |_| {
27876 Err(alloc::format!(
27877 "invalid input syntax for type bigint: \"{t}\""
27878 ))
27879 },
27880 |n| Ok(i128::from(n)),
27881 )),
27882 Expr::Literal(Literal::Bool(_)) => Some(Err(
27883 "argument of {L} must be type bigint, not type boolean".into(),
27884 )),
27885 Expr::Unary {
27886 op: UnOp::Neg,
27887 expr,
27888 } => match fold_limit_constant(expr)? {
27889 Ok(v) => Some(Ok(-v)),
27890 e @ Err(_) => Some(e),
27891 },
27892 Expr::Binary { lhs, op, rhs } => {
27893 let a = match fold_limit_constant(lhs)? {
27894 Ok(v) => v,
27895 e @ Err(_) => return Some(e),
27896 };
27897 let b = match fold_limit_constant(rhs)? {
27898 Ok(v) => v,
27899 e @ Err(_) => return Some(e),
27900 };
27901 let out = match op {
27902 BinOp::Add => a.checked_add(b),
27903 BinOp::Sub => a.checked_sub(b),
27904 BinOp::Mul => a.checked_mul(b),
27905 BinOp::Div if b != 0 => a.checked_div(b),
27906 BinOp::Div => return Some(Err("division by zero".into())),
27907 BinOp::Mod if b != 0 => a.checked_rem(b),
27908 BinOp::Mod => return Some(Err("division by zero".into())),
27909 _ => return None,
27910 };
27911 // PG evaluates the arithmetic in the operand's own type, so an
27912 // int-by-int product that leaves int range fails there — before
27913 // the row count is ever looked at.
27914 match out {
27915 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27916 Some(Err("integer out of range".into()))
27917 }
27918 Some(v) => Some(Ok(v)),
27919 None => Some(Err("integer out of range".into())),
27920 }
27921 }
27922 _ => None,
27923 }
27924}
27925
27926/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27927/// cast, which is what makes `LIMIT 2.5` keep three rows.
27928fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27929 if scale == 0 {
27930 return unscaled;
27931 }
27932 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27933 return 0;
27934 };
27935 let neg = unscaled < 0;
27936 let mag = unscaled.unsigned_abs() as i128;
27937 let rounded = (mag + div / 2) / div;
27938 if neg { -rounded } else { rounded }
27939}
27940
27941#[cfg(test)]
27942mod tests {
27943 use super::*;
27944 use alloc::string::ToString;
27945
27946 fn parse(s: &str) -> Statement {
27947 parse_statement(s).expect("parse ok")
27948 }
27949
27950 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27951 // `tables`, `partition`, etc. are unreserved keywords per PG's
27952 // `pg_get_keywords()` and MUST be usable as column / table /
27953 // alias names. Pre-T4 every drop-in user whose schema had one
27954 // of these as a column name (sentori events.release, mailrs
27955 // messages.index in some forks) blew the parser up at CREATE
27956 // TABLE time with "expected identifier, got Release". The
27957 // generalisation lives in `unreserved_keyword_text` + the
27958 // `expect_ident_like` and `parse_atom` arms that consult it.
27959 #[test]
27960 fn release_usable_as_column_name_in_create_table() {
27961 let stmt =
27962 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27963 if let Statement::CreateTable(t) = stmt {
27964 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27965 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27966 } else {
27967 panic!("expected CreateTable");
27968 }
27969 }
27970
27971 #[test]
27972 fn release_usable_as_column_ref_in_select_projection() {
27973 // The sentori `0003_partition_events.sql` INSERT-SELECT
27974 // walk references `release` in both column lists; the
27975 // projection-side use exercises `parse_atom`'s relaxed
27976 // identifier set.
27977 parse("SELECT id, release, payload FROM events WHERE id = 1");
27978 }
27979
27980 #[test]
27981 fn release_usable_as_column_ref_in_insert_column_list() {
27982 // INSERT INTO t (id, release, payload) VALUES (…)
27983 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27984 }
27985
27986 #[test]
27987 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27988 // Sentori `0013_audit_tombstone.sql` issues
27989 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27990 // emits Token::Drop (not Ident("drop")); the parser must
27991 // accept both in the ALTER COLUMN sub-dispatch.
27992 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27993 }
27994
27995 #[test]
27996 fn create_index_accepts_parenthesised_expression_key() {
27997 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27998 // expression index. Pre-T4 the parser bailed at the
27999 // inner `(` with "expected column ident or expression,
28000 // got LParen". The Token::LParen arm in CREATE INDEX
28001 // routes through the expression parser instead.
28002 parse(
28003 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28004 ON events ((payload->'bundle'->>'id'))",
28005 );
28006 }
28007
28008 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28009 // surface as parse errors, never stack overflows (embed hosts
28010 // abort on overflow).
28011 /// The nesting budget is a COUNT; what it has to fit inside is a
28012 /// number of BYTES, and only one of those two is stable across
28013 /// compiler versions. Round 847 measured 30,336 bytes per level
28014 /// after a toolchain move, which puts 64 levels at 1.94 MB and
28015 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28016 /// aborted instead of erroring, which is precisely the outcome it
28017 /// exists to rule out.
28018 ///
28019 /// So the budget is metered rather than assumed. The ceiling leaves
28020 /// the depth SPG advertises fitting in a default 2 MiB thread with
28021 /// room to spare, in the debug build, where frames are widest.
28022 #[test]
28023 fn nesting_frame_cost_stays_under_ceiling() {
28024 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28025 // thread keeps a margin for whatever called the parser.
28026 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28027
28028 frame_meter::reset();
28029 let depth = frame_meter::SAMPLE_HI + 8;
28030 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28031 parse(&sql);
28032
28033 let per_level = frame_meter::bytes_per_level();
28034 {
28035 extern crate std;
28036 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28037 }
28038 assert!(
28039 per_level <= CEILING,
28040 "{per_level} bytes per nesting level exceeds {CEILING}; \
28041 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28042 in parse_expr_inner / parse_unary rather than lowering the \
28043 depth or widening the stack.",
28044 per_level * MAX_NEST_DEPTH
28045 );
28046 }
28047
28048 #[test]
28049 fn nesting_budget_errors_cleanly() {
28050 let depth = MAX_NEST_DEPTH + 50;
28051 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28052 let err = parse_statement(&sql).expect_err("must reject");
28053 assert!(err.message.contains("nests deeper"), "{err:?}");
28054 // Within budget still parses.
28055 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28056 parse(&sql);
28057 }
28058
28059 #[test]
28060 fn binary_chain_budget_errors_cleanly() {
28061 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28062 let err = parse_statement(&sql).expect_err("must reject");
28063 assert!(err.message.contains("chained binary"), "{err:?}");
28064 // Within budget still parses (chain depth ≤ budget is safe
28065 // for recursive eval/drop on 2 MiB stacks).
28066 let sql = format!("SELECT 1{}", " + 1".repeat(200));
28067 parse(&sql);
28068 }
28069
28070 #[test]
28071 fn in_list_unaffected_by_chain_budget() {
28072 // Flat InList: 20k elements parse fine and stay flat.
28073 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28074 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28075 let Statement::Select(s) = parse(&sql) else {
28076 panic!("expected select")
28077 };
28078 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28079 panic!("expected flat InList, got {:?}", s.where_)
28080 };
28081 assert_eq!(list.len(), 20_000);
28082 assert!(!negated);
28083 }
28084
28085 fn lit_int(n: i64) -> Expr {
28086 Expr::Literal(Literal::Integer(n))
28087 }
28088
28089 fn col(name: &str) -> Expr {
28090 Expr::Column(ColumnName {
28091 qualifier: None,
28092 name: name.into(),
28093 })
28094 }
28095
28096 #[test]
28097 fn select_single_integer() {
28098 let s = parse("SELECT 1");
28099 let Statement::Select(s) = s else {
28100 panic!("expected SELECT")
28101 };
28102 assert_eq!(s.items.len(), 1);
28103 assert!(s.from.is_none());
28104 assert!(s.where_.is_none());
28105 }
28106
28107 #[test]
28108 fn select_multiple_literal_kinds() {
28109 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28110 let Statement::Select(s) = s else {
28111 panic!("expected SELECT")
28112 };
28113 assert_eq!(s.items.len(), 5);
28114 }
28115
28116 #[test]
28117 fn select_wildcard_from_table() {
28118 let s = parse("SELECT * FROM users");
28119 let Statement::Select(s) = s else {
28120 panic!("expected SELECT")
28121 };
28122 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28123 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28124 }
28125
28126 #[test]
28127 fn select_with_table_alias() {
28128 let s = parse("SELECT * FROM users AS u");
28129 let Statement::Select(s) = s else {
28130 panic!("expected SELECT")
28131 };
28132 let t = &s.from.as_ref().unwrap().primary;
28133 assert_eq!(t.name, "users");
28134 assert_eq!(t.alias.as_deref(), Some("u"));
28135 }
28136
28137 #[test]
28138 fn select_with_where_eq() {
28139 let s = parse("SELECT a FROM t WHERE a = 1");
28140 let Statement::Select(s) = s else {
28141 panic!("expected SELECT")
28142 };
28143 let w = s.where_.unwrap();
28144 assert_eq!(
28145 w,
28146 Expr::Binary {
28147 lhs: Box::new(col("a")),
28148 op: BinOp::Eq,
28149 rhs: Box::new(lit_int(1)),
28150 }
28151 );
28152 }
28153
28154 #[test]
28155 fn arithmetic_precedence() {
28156 let s = parse("SELECT 1 + 2 * 3");
28157 let Statement::Select(s) = s else {
28158 panic!("expected SELECT")
28159 };
28160 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28161 panic!("wildcard?")
28162 };
28163 assert_eq!(
28164 expr,
28165 &Expr::Binary {
28166 lhs: Box::new(lit_int(1)),
28167 op: BinOp::Add,
28168 rhs: Box::new(Expr::Binary {
28169 lhs: Box::new(lit_int(2)),
28170 op: BinOp::Mul,
28171 rhs: Box::new(lit_int(3)),
28172 }),
28173 }
28174 );
28175 }
28176
28177 #[test]
28178 fn parentheses_override_precedence() {
28179 let s = parse("SELECT (1 + 2) * 3");
28180 let Statement::Select(s) = s else {
28181 panic!("expected SELECT")
28182 };
28183 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28184 panic!()
28185 };
28186 assert_eq!(
28187 expr,
28188 &Expr::Binary {
28189 lhs: Box::new(Expr::Binary {
28190 lhs: Box::new(lit_int(1)),
28191 op: BinOp::Add,
28192 rhs: Box::new(lit_int(2)),
28193 }),
28194 op: BinOp::Mul,
28195 rhs: Box::new(lit_int(3)),
28196 }
28197 );
28198 }
28199
28200 #[test]
28201 fn not_binds_below_comparison() {
28202 // `NOT a = 1` should parse as `NOT (a = 1)`.
28203 let s = parse("SELECT NOT a = 1 FROM t");
28204 let Statement::Select(s) = s else {
28205 panic!("expected SELECT")
28206 };
28207 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28208 panic!()
28209 };
28210 assert_eq!(
28211 expr,
28212 &Expr::Unary {
28213 op: UnOp::Not,
28214 expr: Box::new(Expr::Binary {
28215 lhs: Box::new(col("a")),
28216 op: BinOp::Eq,
28217 rhs: Box::new(lit_int(1)),
28218 }),
28219 }
28220 );
28221 }
28222
28223 #[test]
28224 fn unary_minus_binds_above_multiplication() {
28225 // `-a * 2` should be `(-a) * 2`.
28226 let s = parse("SELECT -a * 2 FROM t");
28227 let Statement::Select(s) = s else {
28228 panic!("expected SELECT")
28229 };
28230 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28231 panic!()
28232 };
28233 assert_eq!(
28234 expr,
28235 &Expr::Binary {
28236 lhs: Box::new(Expr::Unary {
28237 op: UnOp::Neg,
28238 expr: Box::new(col("a")),
28239 }),
28240 op: BinOp::Mul,
28241 rhs: Box::new(lit_int(2)),
28242 }
28243 );
28244 }
28245
28246 #[test]
28247 fn qualified_column() {
28248 let s = parse("SELECT t.col FROM t");
28249 let Statement::Select(s) = s else {
28250 panic!("expected SELECT")
28251 };
28252 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28253 panic!()
28254 };
28255 assert_eq!(
28256 expr,
28257 &Expr::Column(ColumnName {
28258 qualifier: Some("t".into()),
28259 name: "col".into()
28260 })
28261 );
28262 }
28263
28264 #[test]
28265 fn select_item_alias_with_as() {
28266 let s = parse("SELECT a AS y FROM t");
28267 let Statement::Select(s) = s else {
28268 panic!("expected SELECT")
28269 };
28270 let SelectItem::Expr { alias, .. } = &s.items[0] else {
28271 panic!()
28272 };
28273 assert_eq!(alias.as_deref(), Some("y"));
28274 }
28275
28276 #[test]
28277 fn trailing_semicolon_accepted() {
28278 let s = parse("SELECT 1;");
28279 let Statement::Select(s) = s else {
28280 panic!("expected SELECT")
28281 };
28282 assert_eq!(s.items.len(), 1);
28283 }
28284
28285 #[test]
28286 fn boolean_chain_with_and_or_not() {
28287 // (NOT a) OR (b AND (NOT c))
28288 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28289 let Statement::Select(s) = s else {
28290 panic!("expected SELECT")
28291 };
28292 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28293 panic!()
28294 };
28295 let expected = Expr::Binary {
28296 lhs: Box::new(Expr::Unary {
28297 op: UnOp::Not,
28298 expr: Box::new(col("a")),
28299 }),
28300 op: BinOp::Or,
28301 rhs: Box::new(Expr::Binary {
28302 lhs: Box::new(col("b")),
28303 op: BinOp::And,
28304 rhs: Box::new(Expr::Unary {
28305 op: UnOp::Not,
28306 expr: Box::new(col("c")),
28307 }),
28308 }),
28309 };
28310 assert_eq!(expr, &expected);
28311 }
28312
28313 #[test]
28314 fn empty_input_errors() {
28315 // v7.14.0 — pg_dump preambles emit several comment-only
28316 // / blank-line statements that collapse to Statement::
28317 // Empty rather than a parse error. The old "SELECT in
28318 // message" assertion is stale; verify the new contract:
28319 // empty / whitespace / comment-only input parses to
28320 // Statement::Empty.
28321 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28322 assert!(matches!(
28323 parse_statement(" \n\t ").unwrap(),
28324 Statement::Empty
28325 ));
28326 // Sanity: malformed-but-non-empty still errors.
28327 assert!(parse_statement("SELECT FROM WHERE").is_err());
28328 }
28329
28330 #[test]
28331 fn unmatched_paren_errors() {
28332 assert!(parse_statement("SELECT (1 + 2").is_err());
28333 }
28334
28335 #[test]
28336 fn display_round_trip_simple_select() {
28337 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28338 let text = original.to_string();
28339 let again = parse_statement(&text).expect("re-parse");
28340 assert_eq!(original, again);
28341 }
28342
28343 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28344
28345 #[test]
28346 fn create_table_single_column() {
28347 let s = parse("CREATE TABLE foo (a INT)");
28348 let Statement::CreateTable(c) = s else {
28349 panic!("expected CreateTable")
28350 };
28351 assert_eq!(c.name, "foo");
28352 assert_eq!(c.columns.len(), 1);
28353 assert_eq!(c.columns[0].name, "a");
28354 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28355 assert!(c.columns[0].nullable);
28356 }
28357
28358 #[test]
28359 fn create_table_multi_column_with_not_null_mix() {
28360 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28361 let Statement::CreateTable(c) = s else {
28362 panic!()
28363 };
28364 assert_eq!(c.columns.len(), 4);
28365 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28366 assert!(!c.columns[0].nullable);
28367 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28368 assert!(c.columns[1].nullable);
28369 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28370 assert!(!c.columns[2].nullable);
28371 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28372 }
28373
28374 #[test]
28375 fn create_table_bigint_supported() {
28376 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28377 let Statement::CreateTable(c) = s else {
28378 panic!()
28379 };
28380 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28381 }
28382
28383 #[test]
28384 fn create_table_vector_default_is_f32() {
28385 let s = parse("CREATE TABLE t (v VECTOR(128))");
28386 let Statement::CreateTable(c) = s else {
28387 panic!()
28388 };
28389 assert_eq!(
28390 c.columns[0].ty,
28391 ColumnTypeName::Vector {
28392 dim: 128,
28393 encoding: VecEncoding::F32,
28394 },
28395 );
28396 }
28397
28398 #[test]
28399 fn create_table_vector_using_sq8() {
28400 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28401 // Case-insensitive on both `USING` and the encoding name.
28402 for sql in [
28403 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28404 "CREATE TABLE t (v VECTOR(128) using sq8)",
28405 ] {
28406 let s = parse(sql);
28407 let Statement::CreateTable(c) = s else {
28408 panic!()
28409 };
28410 assert_eq!(
28411 c.columns[0].ty,
28412 ColumnTypeName::Vector {
28413 dim: 128,
28414 encoding: VecEncoding::Sq8,
28415 },
28416 "{sql}",
28417 );
28418 }
28419 }
28420
28421 #[test]
28422 fn create_table_vector_using_unknown_errors() {
28423 // v7.16.1 — the inline `USING <encoding>` shape on
28424 // CREATE TABLE column defs was withdrawn before
28425 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28426 // (col vector_<metric>_ops)`; the parser now rejects
28427 // USING at column-list position with a clearer
28428 // "expected ',' or ')'" message. Test asserts the
28429 // current rejection, not the old "unknown vector
28430 // encoding" string.
28431 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28432 assert!(
28433 err.message.contains("USING")
28434 || err.message.contains("using")
28435 || err.message.contains("')'")
28436 || err.message.contains("','"),
28437 "expected USING/column-list rejection, got: {}",
28438 err.message
28439 );
28440 }
28441
28442 #[test]
28443 fn vector_using_sq8_display_roundtrips() {
28444 // The Display impl must produce text that re-parses to the
28445 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28446 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28447 let Statement::CreateTable(c) = s else {
28448 panic!()
28449 };
28450 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28451 }
28452
28453 #[test]
28454 fn parser_recognises_placeholders() {
28455 use crate::ast::{Expr, SelectItem, Statement};
28456 // $N in expression position parses as Expr::Placeholder(N).
28457 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28458 let Statement::Select(sel) = s else { panic!() };
28459 assert!(matches!(
28460 sel.items[0],
28461 SelectItem::Expr {
28462 expr: Expr::Placeholder(1),
28463 alias: None
28464 }
28465 ));
28466 // $2 + 1
28467 let SelectItem::Expr {
28468 expr: Expr::Binary { lhs, rhs, .. },
28469 ..
28470 } = &sel.items[1]
28471 else {
28472 panic!()
28473 };
28474 assert!(matches!(**lhs, Expr::Placeholder(2)));
28475 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28476 // WHERE x = $3
28477 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28478 panic!()
28479 };
28480 assert!(matches!(**rhs, Expr::Placeholder(3)));
28481 }
28482
28483 #[test]
28484 fn parser_rejects_dollar_zero() {
28485 // $0 is not valid in PG; the lexer rejects it.
28486 assert!(parse_statement("SELECT $0").is_err());
28487 }
28488
28489 #[test]
28490 fn placeholder_display_roundtrips() {
28491 // The Display impl must produce text that re-lexes to the
28492 // same Placeholder token.
28493 let s = parse("SELECT $42 FROM t");
28494 let printed = s.to_string();
28495 assert!(printed.contains("$42"));
28496 let again = parse(&printed);
28497 assert_eq!(s, again);
28498 }
28499
28500 #[test]
28501 fn alter_index_rebuild_bare() {
28502 use crate::ast::{AlterIndexTarget, Statement};
28503 let s = parse("ALTER INDEX my_idx REBUILD");
28504 let Statement::AlterIndex(a) = s else {
28505 panic!("expected AlterIndex, got {s:?}")
28506 };
28507 assert_eq!(a.name, "my_idx");
28508 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28509 }
28510
28511 #[test]
28512 fn alter_index_rebuild_with_encoding() {
28513 use crate::ast::{AlterIndexTarget, Statement};
28514 for (sql, want) in [
28515 (
28516 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28517 VecEncoding::F32,
28518 ),
28519 (
28520 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28521 VecEncoding::Sq8,
28522 ),
28523 (
28524 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28525 VecEncoding::F16,
28526 ),
28527 ] {
28528 let s = parse(sql);
28529 let Statement::AlterIndex(a) = s else {
28530 panic!("{sql}: expected AlterIndex")
28531 };
28532 assert_eq!(a.name, "my_idx");
28533 assert_eq!(
28534 a.target,
28535 AlterIndexTarget::Rebuild {
28536 encoding: Some(want)
28537 },
28538 "{sql}"
28539 );
28540 }
28541 }
28542
28543 #[test]
28544 fn alter_index_rebuild_unknown_encoding_errors() {
28545 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28546 assert!(
28547 err.message.contains("unknown vector encoding"),
28548 "got: {}",
28549 err.message
28550 );
28551 }
28552
28553 #[test]
28554 fn alter_index_rebuild_display_roundtrips() {
28555 for (input, want) in [
28556 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28557 (
28558 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28559 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28560 ),
28561 (
28562 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28563 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28564 ),
28565 ] {
28566 let s = parse(input);
28567 assert_eq!(s.to_string(), want);
28568 }
28569 }
28570
28571 #[test]
28572 fn create_table_unknown_type_defers_to_engine() {
28573 // v4.9 picked XML as a parse-time "unsupported column
28574 // type" probe. v7.17.0 Phase 1.4 changed the contract:
28575 // an unknown type ident parses as Text + `user_type_ref`
28576 // so CREATE TABLE can resolve user-defined enum / domain
28577 // types — rejection of truly-unknown types moved to the
28578 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28579 // to a first-class built-in, so this probe switched to a
28580 // synthetic name nothing in the lexer will ever recognise.
28581 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28582 let Statement::CreateTable(t) = stmt else {
28583 panic!("expected CreateTable");
28584 };
28585 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28586 }
28587
28588 #[test]
28589 fn create_table_missing_table_keyword_errors() {
28590 assert!(parse_statement("CREATE x (a INT)").is_err());
28591 }
28592
28593 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28594 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28595
28596 #[test]
28597 fn parse_create_table_partition_by_range() {
28598 use crate::ast::{PartitionBySpec, PartitionKindAst};
28599 let stmt = parse_statement(
28600 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28601 payload JSONB) PARTITION BY RANGE (ts)",
28602 )
28603 .unwrap();
28604 let Statement::CreateTable(t) = stmt else {
28605 panic!("expected CreateTable");
28606 };
28607 assert!(t.partition_of.is_none(), "parent has no partition_of");
28608 assert_eq!(t.columns.len(), 3);
28609 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28610 assert_eq!(
28611 by,
28612 &PartitionBySpec {
28613 kind: PartitionKindAst::Range,
28614 key_columns: alloc::vec!["ts".to_string()],
28615 }
28616 );
28617 // Display round-trip preserves the suffix. `quote_ident`
28618 // only adds double quotes when the ident needs escaping, so
28619 // a plain `ts` survives bare here.
28620 assert!(
28621 t.to_string().contains("PARTITION BY RANGE (ts)"),
28622 "Display lost PARTITION BY suffix: {t}"
28623 );
28624 }
28625
28626 #[test]
28627 fn parse_create_table_partition_of_range() {
28628 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28629 let stmt = parse_statement(
28630 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28631 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28632 )
28633 .unwrap();
28634 let Statement::CreateTable(t) = stmt else {
28635 panic!("expected CreateTable");
28636 };
28637 assert!(t.columns.is_empty(), "child inherits columns from parent");
28638 assert!(t.partition_by.is_none());
28639 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28640 assert_eq!(of.parent_name, "events_partitioned");
28641 let PartitionOfSpec { bounds, .. } = of.clone();
28642 match bounds {
28643 PartitionOfBoundsAst::Range { lower, upper } => {
28644 assert!(lower.to_string().contains("2026-06-01"));
28645 assert!(upper.to_string().contains("2026-07-01"));
28646 }
28647 other => panic!("expected Range, got {other:?}"),
28648 }
28649 // Display round-trip emits the FOR VALUES tail. `quote_ident`
28650 // skips quotes when not required, so the parent name appears
28651 // bare here.
28652 let s = t.to_string();
28653 assert!(
28654 s.contains("PARTITION OF events_partitioned"),
28655 "Display lost PARTITION OF: {s}"
28656 );
28657 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28658 assert!(s.contains(") TO ("), "Display lost TO: {s}");
28659 }
28660
28661 #[test]
28662 fn parse_create_table_partition_of_default() {
28663 use crate::ast::PartitionOfBoundsAst;
28664 let stmt =
28665 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28666 .unwrap();
28667 let Statement::CreateTable(t) = stmt else {
28668 panic!("expected CreateTable");
28669 };
28670 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28671 assert_eq!(of.parent_name, "events_partitioned");
28672 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28673 assert!(
28674 t.to_string()
28675 .contains("PARTITION OF events_partitioned DEFAULT"),
28676 "Display lost DEFAULT: {t}"
28677 );
28678 }
28679
28680 #[test]
28681 fn parse_create_table_partition_by_list() {
28682 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28683 // child with `FOR VALUES IN (lit, lit, …)`.
28684 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28685 let parent =
28686 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28687 .unwrap();
28688 let Statement::CreateTable(t) = parent else {
28689 panic!("expected CreateTable");
28690 };
28691 let Some(PartitionBySpec {
28692 kind,
28693 ref key_columns,
28694 }) = t.partition_by
28695 else {
28696 panic!("expected PARTITION BY");
28697 };
28698 assert_eq!(kind, PartitionKindAst::List);
28699 assert_eq!(*key_columns, vec!["region".to_string()]);
28700 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28701
28702 let child = parse_statement(
28703 "CREATE TABLE events_apac PARTITION OF events_listed \
28704 FOR VALUES IN ('jp', 'kr', 'tw')",
28705 )
28706 .unwrap();
28707 let Statement::CreateTable(c) = child else {
28708 panic!("expected CreateTable");
28709 };
28710 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28711 let PartitionOfBoundsAst::List { values } = &of.bounds else {
28712 panic!("expected List bounds, got {:?}", of.bounds);
28713 };
28714 assert_eq!(values.len(), 3);
28715 let disp = c.to_string();
28716 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28717 }
28718
28719 #[test]
28720 fn parse_create_table_partition_by_hash() {
28721 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28722 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28723 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28724 let parent =
28725 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28726 let Statement::CreateTable(t) = parent else {
28727 panic!("expected CreateTable");
28728 };
28729 let Some(PartitionBySpec {
28730 kind,
28731 ref key_columns,
28732 }) = t.partition_by
28733 else {
28734 panic!("expected PARTITION BY");
28735 };
28736 assert_eq!(kind, PartitionKindAst::Hash);
28737 assert_eq!(*key_columns, vec!["id".to_string()]);
28738 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28739
28740 let child = parse_statement(
28741 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28742 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28743 )
28744 .unwrap();
28745 let Statement::CreateTable(c) = child else {
28746 panic!("expected CreateTable");
28747 };
28748 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28749 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28750 panic!("expected Hash bounds");
28751 };
28752 assert_eq!(modulus, 4);
28753 assert_eq!(remainder, 0);
28754 let disp = c.to_string();
28755 assert!(
28756 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28757 "Display lost HASH bounds: {disp}"
28758 );
28759
28760 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28761 let bad = parse_statement(
28762 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28763 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28764 );
28765 let msg = format!("{}", bad.unwrap_err());
28766 assert!(
28767 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28768 "expected REMAINDER/MODULUS validation error: {msg}"
28769 );
28770 }
28771
28772 #[test]
28773 fn parse_create_table_partition_of_rejects_columns() {
28774 // v7.37.6-B contract: PARTITION OF children inherit columns
28775 // from the parent; an explicit list MUST surface as a parse
28776 // error rather than getting silently ignored.
28777 let err = parse_statement(
28778 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28779 FOR VALUES FROM ('a') TO ('b')",
28780 );
28781 assert!(err.is_err(), "expected parse error for explicit columns");
28782 let msg = format!("{}", err.unwrap_err());
28783 assert!(
28784 msg.contains("PARTITION OF") && msg.contains("column"),
28785 "error should mention PARTITION OF + columns: {msg}"
28786 );
28787 }
28788
28789 #[test]
28790 fn insert_single_value() {
28791 let s = parse("INSERT INTO foo VALUES (42)");
28792 let Statement::Insert(i) = s else {
28793 panic!("expected Insert")
28794 };
28795 assert_eq!(i.table, "foo");
28796 assert_eq!(i.rows.len(), 1);
28797 assert_eq!(i.rows[0].len(), 1);
28798 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28799 }
28800
28801 #[test]
28802 fn insert_multi_value_with_mixed_literals() {
28803 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28804 let Statement::Insert(i) = s else { panic!() };
28805 assert_eq!(i.rows.len(), 1);
28806 assert_eq!(i.rows[0].len(), 5);
28807 }
28808
28809 #[test]
28810 fn insert_missing_into_errors() {
28811 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28812 }
28813
28814 #[test]
28815 fn create_table_round_trip() {
28816 let original =
28817 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28818 let text = original.to_string();
28819 let again = parse_statement(&text).expect("re-parse");
28820 assert_eq!(original, again);
28821 }
28822
28823 #[test]
28824 fn insert_round_trip_with_negation_and_string() {
28825 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28826 let text = original.to_string();
28827 let again = parse_statement(&text).expect("re-parse");
28828 assert_eq!(original, again);
28829 }
28830
28831 #[test]
28832 fn unknown_keyword_at_statement_start_errors() {
28833 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28834 // the top-level dispatch still has no branch to take.
28835 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28836 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28837 }
28838
28839 // --- v0.8 CREATE INDEX --------------------------------------------------
28840
28841 #[test]
28842 fn create_index_basic() {
28843 let s = parse("CREATE INDEX idx_id ON users (id)");
28844 let Statement::CreateIndex(c) = s else {
28845 panic!("expected CreateIndex")
28846 };
28847 assert_eq!(c.name, "idx_id");
28848 assert_eq!(c.table, "users");
28849 assert_eq!(c.column, "id");
28850 }
28851
28852 #[test]
28853 fn create_index_missing_on_errors() {
28854 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28855 }
28856
28857 #[test]
28858 fn create_index_missing_paren_errors() {
28859 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28860 }
28861
28862 #[test]
28863 fn create_index_round_trip() {
28864 let original = parse("CREATE INDEX by_name ON users (name)");
28865 let again = parse_statement(&original.to_string()).unwrap();
28866 assert_eq!(original, again);
28867 }
28868
28869 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28870
28871 #[test]
28872 fn create_unique_index_basic() {
28873 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28874 let Statement::CreateIndex(c) = s else {
28875 panic!("expected CreateIndex");
28876 };
28877 assert!(c.is_unique);
28878 assert_eq!(c.column, "a");
28879 assert!(c.partial_predicate.is_none());
28880 }
28881
28882 #[test]
28883 fn create_unique_index_partial() {
28884 // mailrs's email_templates "one default per user" shape.
28885 let s = parse(
28886 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28887 ON email_templates (user_address) WHERE is_default = true",
28888 );
28889 let Statement::CreateIndex(c) = s else {
28890 panic!("expected CreateIndex");
28891 };
28892 assert!(c.is_unique);
28893 assert_eq!(c.table, "email_templates");
28894 assert_eq!(c.column, "user_address");
28895 assert!(c.partial_predicate.is_some());
28896 }
28897
28898 #[test]
28899 fn create_unique_index_composite_with_predicate() {
28900 // mailrs's calendar_events instance: composite columns.
28901 let s = parse(
28902 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28903 ON calendar_events (calendar_id, uid, recurrence_id) \
28904 WHERE recurrence_id IS NOT NULL",
28905 );
28906 let Statement::CreateIndex(c) = s else {
28907 panic!("expected CreateIndex");
28908 };
28909 assert!(c.is_unique);
28910 assert_eq!(c.column, "calendar_id");
28911 assert_eq!(
28912 c.extra_columns,
28913 vec!["uid".to_string(), "recurrence_id".to_string()]
28914 );
28915 assert!(c.partial_predicate.is_some());
28916 }
28917
28918 #[test]
28919 fn create_unique_index_using_btree_ok() {
28920 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28921 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28922 }
28923
28924 #[test]
28925 fn create_unique_index_using_hnsw_rejected() {
28926 let err =
28927 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28928 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28929 }
28930
28931 #[test]
28932 fn create_unique_index_round_trip() {
28933 let original = parse(
28934 "CREATE UNIQUE INDEX uq_calendar_events_master \
28935 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28936 );
28937 let again = parse_statement(&original.to_string()).unwrap();
28938 assert_eq!(original, again);
28939 }
28940
28941 #[test]
28942 fn create_unique_without_index_errors() {
28943 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28944 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28945 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28946 }
28947
28948 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28949
28950 #[test]
28951 fn create_table_bytea_column() {
28952 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28953 let Statement::CreateTable(c) = s else {
28954 panic!("expected CreateTable");
28955 };
28956 assert_eq!(c.columns.len(), 2);
28957 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28958 assert!(!c.columns[1].nullable);
28959 }
28960
28961 #[test]
28962 fn create_table_bytes_alias_column() {
28963 let s = parse("CREATE TABLE t (blob BYTES)");
28964 let Statement::CreateTable(c) = s else {
28965 panic!("expected CreateTable");
28966 };
28967 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28968 }
28969
28970 #[test]
28971 fn bytea_round_trip_display() {
28972 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28973 let again = parse_statement(&original.to_string()).unwrap();
28974 assert_eq!(original, again);
28975 }
28976
28977 // --- v0.9 transactions -------------------------------------------------
28978
28979 #[test]
28980 fn begin_commit_rollback_parse_as_unit_variants() {
28981 let plain = crate::ast::TransactionModes::default();
28982 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
28983 assert_eq!(parse("COMMIT"), Statement::Commit);
28984 // r1066 — PG synonyms pgbench's tpcb script relies on.
28985 assert_eq!(parse("END"), Statement::Commit);
28986 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28987 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28988 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28989 // Trailing semicolons accepted too.
28990 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
28991 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28992 // statement (with or without the WORK/TRANSACTION noise word).
28993 assert_eq!(
28994 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28995 Statement::Begin(crate::ast::TransactionModes {
28996 isolation: Some(IsolationLevel::RepeatableRead),
28997 read_only: None,
28998 })
28999 );
29000 assert_eq!(
29001 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29002 Statement::Begin(crate::ast::TransactionModes {
29003 isolation: Some(IsolationLevel::Serializable),
29004 read_only: None,
29005 })
29006 );
29007 // v7.39 — this line used to read
29008 //
29009 // // A non-isolation mode keeps the session default (None).
29010 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29011 //
29012 // which pinned the defect rather than catching it: the READ ONLY
29013 // was thrown away, so the statement opened an ordinary read-write
29014 // transaction and every write inside it was accepted. The
29015 // isolation level is still absent here, because this statement
29016 // does not name one — that part was right.
29017 assert_eq!(
29018 parse("BEGIN READ ONLY"),
29019 Statement::Begin(crate::ast::TransactionModes {
29020 isolation: None,
29021 read_only: Some(true),
29022 })
29023 );
29024 assert_eq!(
29025 parse("START TRANSACTION READ WRITE"),
29026 Statement::Begin(crate::ast::TransactionModes {
29027 isolation: None,
29028 read_only: Some(false),
29029 })
29030 );
29031 assert_eq!(
29032 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29033 Statement::Begin(crate::ast::TransactionModes {
29034 isolation: Some(IsolationLevel::Serializable),
29035 read_only: Some(true),
29036 })
29037 );
29038 }
29039
29040 // --- v1.2: pgvector distance ops + ::vector cast --------------------
29041
29042 #[test]
29043 fn inner_product_binop_parses() {
29044 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29045 let Statement::Select(s) = s else { panic!() };
29046 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29047 panic!()
29048 };
29049 assert!(matches!(
29050 expr,
29051 Expr::Binary {
29052 op: BinOp::InnerProduct,
29053 ..
29054 }
29055 ));
29056 }
29057
29058 #[test]
29059 fn cosine_distance_binop_parses() {
29060 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29061 let Statement::Select(s) = s else { panic!() };
29062 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29063 panic!()
29064 };
29065 assert!(matches!(
29066 expr,
29067 Expr::Binary {
29068 op: BinOp::CosineDistance,
29069 ..
29070 }
29071 ));
29072 }
29073
29074 #[test]
29075 fn vector_cast_postfix_wraps_string_literal() {
29076 let s = parse("SELECT '[1,2,3]'::vector FROM t");
29077 let Statement::Select(s) = s else { panic!() };
29078 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29079 panic!()
29080 };
29081 assert!(matches!(
29082 expr,
29083 Expr::Cast {
29084 target: CastTarget::Vector,
29085 ..
29086 }
29087 ));
29088 }
29089
29090 #[test]
29091 fn unsupported_cast_target_errors() {
29092 // v7.37.5 ship triage promoted the parser to accept every
29093 // ident as a `CastTarget::Named(canonical)`; the engine
29094 // surfaces the "unsupported cast target" error at eval
29095 // time when `type_name_to_data_type` can't resolve it.
29096 // Parser-side error now requires a NON-ident after `::`
29097 // (e.g. a punctuation token).
29098 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29099 assert_eq!(err.message, "syntax error at or near \",\"");
29100 }
29101
29102 #[test]
29103 fn tx_statements_round_trip() {
29104 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29105 let original = parse(q);
29106 let again = parse_statement(&original.to_string()).unwrap();
29107 assert_eq!(original, again);
29108 }
29109 }
29110
29111 #[test]
29112 fn interval_text_parsing_units() {
29113 // v7.37.5 β — three-field shape `(months, days, micros)` so
29114 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29115 // Single unit.
29116 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29117 assert_eq!(
29118 parse_interval_text("24 hours"),
29119 Some((0, 0, 86_400_000_000))
29120 );
29121 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29122 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29123 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29124 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29125 // Compound spans accumulate per-dimension.
29126 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29127 assert_eq!(
29128 parse_interval_text("1 day 2 hours"),
29129 Some((0, 1, 7_200_000_000))
29130 );
29131 // Negative numbers carry through per-dimension.
29132 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29133 // Bad shapes return None.
29134 assert_eq!(parse_interval_text(""), None);
29135 assert_eq!(parse_interval_text("garbage"), None);
29136 assert_eq!(parse_interval_text("1 fortnight"), None);
29137 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29138 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29139 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29140 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29141 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29142 }
29143
29144 #[test]
29145 fn interval_literal_roundtrips_via_display() {
29146 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29147 let s = parsed.to_string();
29148 // Display preserves the original text verbatim.
29149 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29150 // And re-parsing yields a structurally equal statement.
29151 let again = parse_statement(&s).unwrap();
29152 assert_eq!(parsed, again);
29153 }
29154
29155 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29156
29157 #[test]
29158 fn parser_recognises_create_publication_bare() {
29159 let s = parse("CREATE PUBLICATION pub_a");
29160 let Statement::CreatePublication(p) = s else {
29161 panic!("expected CreatePublication, got {s:?}")
29162 };
29163 assert_eq!(p.name, "pub_a");
29164 assert_eq!(p.scope, PublicationScope::AllTables);
29165 }
29166
29167 #[test]
29168 fn parser_recognises_create_publication_for_all_tables() {
29169 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29170 let Statement::CreatePublication(p) = s else {
29171 panic!("expected CreatePublication, got {s:?}")
29172 };
29173 assert_eq!(p.name, "pub_a");
29174 assert_eq!(p.scope, PublicationScope::AllTables);
29175 }
29176
29177 #[test]
29178 fn parser_recognises_drop_publication() {
29179 let s = parse("DROP PUBLICATION pub_a");
29180 let Statement::DropPublication { name, .. } = s else {
29181 panic!("expected DropPublication, got {s:?}")
29182 };
29183 assert_eq!(name, "pub_a");
29184 }
29185
29186 #[test]
29187 fn parser_recognises_for_table_list() {
29188 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29189 let Statement::CreatePublication(p) = s else {
29190 panic!("expected CreatePublication, got {s:?}")
29191 };
29192 assert_eq!(p.name, "pub_a");
29193 let PublicationScope::ForTables(ts) = p.scope else {
29194 panic!("expected ForTables scope")
29195 };
29196 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29197 }
29198
29199 #[test]
29200 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29201 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29202 // is rejected (`invalid publication object list`; the old
29203 // test pinned an unverifiable "PG 19 accepts both" claim);
29204 // TABLES pairs with IN SCHEMA.
29205 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29206 .expect_err("bare FOR TABLES must reject");
29207 assert!(
29208 alloc::format!("{err}").contains("invalid publication object list"),
29209 "got: {err}"
29210 );
29211 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29212 let Statement::CreatePublication(p) = s else {
29213 panic!("expected CreatePublication, got {s:?}")
29214 };
29215 let PublicationScope::TablesInSchema(schema) = p.scope else {
29216 panic!("expected TablesInSchema")
29217 };
29218 assert_eq!(schema, "public");
29219 }
29220
29221 #[test]
29222 fn parser_recognises_for_all_tables_except_list() {
29223 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29224 let Statement::CreatePublication(p) = s else {
29225 panic!()
29226 };
29227 let PublicationScope::AllTablesExcept(ts) = p.scope else {
29228 panic!("expected AllTablesExcept")
29229 };
29230 assert_eq!(ts, alloc::vec!["t1", "t2"]);
29231 }
29232
29233 #[test]
29234 fn parser_rejects_for_table_with_empty_list() {
29235 // `FOR TABLE` with nothing after is a parse error.
29236 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29237 .expect_err("must error on empty list");
29238 // No specific message asserted — the call falls through to
29239 // expect_ident_like which yields "expected identifier, got …".
29240 assert!(!err.message.is_empty());
29241 }
29242
29243 #[test]
29244 fn parser_recognises_show_publications() {
29245 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29246 // bare ident in this position, NOT a reserved keyword.
29247 let s = parse("SHOW PUBLICATIONS");
29248 assert!(matches!(s, Statement::ShowPublications));
29249 }
29250
29251 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29252
29253 #[test]
29254 fn parser_recognises_create_subscription_single_publication() {
29255 let s = parse(
29256 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29257 );
29258 let Statement::CreateSubscription(c) = s else {
29259 panic!("expected CreateSubscription, got {s:?}")
29260 };
29261 assert_eq!(c.name, "sub_a");
29262 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29263 assert_eq!(c.publications, alloc::vec!["pub_a"]);
29264 }
29265
29266 #[test]
29267 fn parser_recognises_create_subscription_multi_publication() {
29268 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29269 let Statement::CreateSubscription(c) = s else {
29270 panic!()
29271 };
29272 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29273 }
29274
29275 #[test]
29276 fn parser_rejects_create_subscription_missing_connection() {
29277 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29278 .expect_err("must error on missing CONNECTION");
29279 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29280 }
29281
29282 #[test]
29283 fn parser_rejects_create_subscription_missing_publication() {
29284 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29285 .expect_err("must error on missing PUBLICATION");
29286 assert_eq!(err.message, "syntax error at end of input");
29287 }
29288
29289 #[test]
29290 fn parser_recognises_drop_subscription() {
29291 let s = parse("DROP SUBSCRIPTION sub_a");
29292 let Statement::DropSubscription { name, .. } = s else {
29293 panic!("expected DropSubscription, got {s:?}")
29294 };
29295 assert_eq!(name, "sub_a");
29296 }
29297
29298 #[test]
29299 fn parser_recognises_show_subscriptions() {
29300 let s = parse("SHOW SUBSCRIPTIONS");
29301 assert!(matches!(s, Statement::ShowSubscriptions));
29302 }
29303
29304 #[test]
29305 fn parser_recognises_wait_for_wal_position_no_timeout() {
29306 let s = parse("WAIT FOR WAL POSITION 12345");
29307 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29308 panic!("expected WaitForWalPosition, got {s:?}")
29309 };
29310 assert_eq!(pos, 12345);
29311 assert!(timeout_ms.is_none());
29312 }
29313
29314 #[test]
29315 fn parser_recognises_wait_for_wal_position_with_timeout() {
29316 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29317 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29318 panic!()
29319 };
29320 assert_eq!(pos, 67890);
29321 assert_eq!(timeout_ms, Some(5000));
29322 }
29323
29324 #[test]
29325 fn parser_rejects_wait_with_negative_position() {
29326 // The lexer treats `-` as a token; `expect_u64_literal`
29327 // only sees the Integer that follows, so the negative
29328 // arrives as a unary-minus expression at higher levels.
29329 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29330 // parse error one way or another.
29331 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29332 assert!(!err.message.is_empty());
29333 }
29334
29335 #[test]
29336 fn parser_recognises_bare_analyze() {
29337 let s = parse("ANALYZE");
29338 assert!(matches!(s, Statement::Analyze(None)));
29339 }
29340
29341 #[test]
29342 fn parser_recognises_analyze_with_table() {
29343 let s = parse("ANALYZE users");
29344 let Statement::Analyze(Some(name)) = s else {
29345 panic!("expected Analyze, got {s:?}")
29346 };
29347 assert_eq!(name, "users");
29348 }
29349
29350 #[test]
29351 fn parser_recognises_analyze_with_quoted_table() {
29352 let s = parse("ANALYZE \"Mixed Case\"");
29353 let Statement::Analyze(Some(name)) = s else {
29354 panic!()
29355 };
29356 assert_eq!(name, "Mixed Case");
29357 }
29358
29359 #[test]
29360 fn parser_rejects_analyze_with_garbage_token() {
29361 let err = parse_statement("ANALYZE 42").expect_err("must error");
29362 assert!(!err.message.is_empty());
29363 }
29364
29365 #[test]
29366 fn analyze_display_roundtrips() {
29367 for sql in ["ANALYZE", "ANALYZE users"] {
29368 let s = parse(sql);
29369 let printed = s.to_string();
29370 let again = parse_statement(&printed)
29371 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29372 assert_eq!(s, again);
29373 }
29374 }
29375
29376 #[test]
29377 fn wait_for_display_roundtrips() {
29378 for sql in [
29379 "WAIT FOR WAL POSITION 12345",
29380 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29381 ] {
29382 let s = parse(sql);
29383 let printed = s.to_string();
29384 let again = parse_statement(&printed)
29385 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29386 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29387 }
29388 }
29389
29390 #[test]
29391 fn subscription_ddl_display_roundtrips() {
29392 for sql in [
29393 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29394 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29395 "DROP SUBSCRIPTION sub_a",
29396 "SHOW SUBSCRIPTIONS",
29397 ] {
29398 let s = parse(sql);
29399 let printed = s.to_string();
29400 let again = parse_statement(&printed)
29401 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29402 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29403 }
29404 }
29405
29406 #[test]
29407 fn parser_drop_dispatches_user_vs_publication() {
29408 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29409 // tokenises DROP. Both targets must still parse.
29410 let s = parse("DROP USER 'alice'");
29411 let Statement::DropUser { name, .. } = s else {
29412 panic!("expected DropUser, got {s:?}")
29413 };
29414 assert_eq!(name, "alice");
29415 // And DROP PUBLICATION lands the new variant.
29416 let s = parse("DROP PUBLICATION p1");
29417 assert!(matches!(s, Statement::DropPublication { .. }));
29418 }
29419
29420 #[test]
29421 fn publication_ddl_display_roundtrips() {
29422 // Every CREATE PUBLICATION variant must Display → parse →
29423 // same AST. v6.1.3 covers all three scope shapes.
29424 for sql in [
29425 "CREATE PUBLICATION pub_a",
29426 "CREATE PUBLICATION pub_a FOR ALL TABLES",
29427 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29428 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29429 "DROP PUBLICATION pub_a",
29430 "SHOW PUBLICATIONS",
29431 ] {
29432 let s = parse(sql);
29433 let printed = s.to_string();
29434 let again = parse_statement(&printed)
29435 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29436 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29437 }
29438 }
29439
29440 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29441
29442 #[test]
29443 fn create_function_returns_trigger_plpgsql_minimal() {
29444 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29445 let s = parse(sql);
29446 let Statement::CreateFunction(f) = s else {
29447 panic!("expected CreateFunction");
29448 };
29449 assert_eq!(f.name, "noop");
29450 assert!(!f.or_replace);
29451 assert!(f.args.is_empty());
29452 assert!(matches!(f.returns, FunctionReturn::Trigger));
29453 assert_eq!(f.language, "plpgsql");
29454 let FunctionBody::PlPgSql(block) = f.body else {
29455 panic!("expected PlPgSql body");
29456 };
29457 assert_eq!(block.statements.len(), 1);
29458 assert!(matches!(
29459 block.statements[0],
29460 PlPgSqlStmt::Return(ReturnTarget::New)
29461 ));
29462 }
29463
29464 #[test]
29465 fn create_function_or_replace_with_assignment() {
29466 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29467 // RETURN NEW.
29468 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29469BEGIN
29470 NEW.search_vector := to_tsvector('english', NEW.subject);
29471 RETURN NEW;
29472END;
29473$$";
29474 let s = parse(sql);
29475 let Statement::CreateFunction(f) = s else {
29476 panic!("expected CreateFunction");
29477 };
29478 assert!(f.or_replace);
29479 let FunctionBody::PlPgSql(block) = &f.body else {
29480 panic!("expected PlPgSql body");
29481 };
29482 assert_eq!(block.statements.len(), 2);
29483 // First statement: NEW.search_vector := to_tsvector(...)
29484 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29485 panic!("expected Assign as first stmt");
29486 };
29487 match target {
29488 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29489 other => panic!("expected NEW.col, got {other:?}"),
29490 }
29491 // Second statement: RETURN NEW
29492 assert!(matches!(
29493 block.statements[1],
29494 PlPgSqlStmt::Return(ReturnTarget::New)
29495 ));
29496 }
29497
29498 #[test]
29499 fn create_trigger_after_insert_or_update() {
29500 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29501 let s = parse(sql);
29502 let Statement::CreateTrigger(t) = s else {
29503 panic!("expected CreateTrigger");
29504 };
29505 assert_eq!(t.name, "tg");
29506 assert_eq!(t.table, "messages");
29507 assert_eq!(t.timing, TriggerTiming::After);
29508 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29509 assert_eq!(t.for_each, TriggerForEach::Row);
29510 assert_eq!(t.function, "update_sv");
29511 }
29512
29513 #[test]
29514 fn create_trigger_before_delete_execute_procedure_alias() {
29515 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29516 let sql =
29517 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29518 let s = parse(sql);
29519 let Statement::CreateTrigger(t) = s else {
29520 panic!("expected CreateTrigger");
29521 };
29522 assert_eq!(t.timing, TriggerTiming::Before);
29523 assert_eq!(t.events, vec![TriggerEvent::Delete]);
29524 }
29525
29526 #[test]
29527 fn drop_trigger_if_exists_round_trips() {
29528 // No parser support for DROP TRIGGER yet — added in v7.12.5
29529 // alongside the broader DROP …{IF EXISTS} cleanup. The
29530 // AST + Display impls are in place so we round-trip via
29531 // construction:
29532 let s = Statement::DropTrigger {
29533 name: "tg".into(),
29534 table: "messages".into(),
29535 if_exists: true,
29536 };
29537 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29538 }
29539
29540 #[test]
29541 fn trigger_ddl_display_roundtrips_through_parser() {
29542 // CREATE TRIGGER + its referenced CREATE FUNCTION must
29543 // Display → parse → same AST (modulo PL/pgSQL body
29544 // formatting which is parser-canonicalised).
29545 for sql in [
29546 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29547 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29548 ] {
29549 let s = parse(sql);
29550 let printed = s.to_string();
29551 let again = parse_statement(&printed)
29552 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29553 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29554 }
29555 }
29556}